---
title: "1.3.8 How to choose a market data provider"
description: "Criteria for choosing a data provider: coverage, history, delisted stocks, adjustments, license and cost, measured on a universe with a weighted matrix."
url: "https://pythonparatrading.com/en/python-trading-course/level-1/how-to-choose-a-data-provider/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-1/how-to-choose-a-data-provider.png"
---

# 1.3.8 · How to choose a market data provider

> A provider is not chosen by price or by how convenient its API is, but by the biases it introduces into your results. This lesson measures those biases on a fictional 40-stock index and ends with a weighted decision matrix and an audit you can run on any sample.

- 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/), [1.3.2 · Free market data sources](https://pythonparatrading.com/en/python-trading-course/level-1/free-market-data-sources/), [1.3.7 · Market data licensing and terms of use](https://pythonparatrading.com/en/python-trading-course/level-1/market-data-licensing-and-terms-of-use/)

## By the end of this lesson

- List the criteria that separate a useful provider from a dangerous one: coverage, historical depth, delisted securities, corporate adjustments, identifiers, license and cost.
- Measure with pandas how much a provider without delisted securities or without membership history inflates an index's return.
- Detect in a price sample whether the provider adjusts for splits and dividends.
- Build a decision matrix with knockout criteria and weights that depend on the use case, and see how the choice changes.

## Choosing a provider is choosing biases

The previous lessons in the module go through the sources: free (1.3.2), retail APIs (1.3.3), professional (1.3.4),
crypto (1.3.5), brokers (1.3.6) and their licenses (1.3.7). All of them sell “market data”, but they do not sell the
same thing. Two providers can give the same close for today and, at the same time, ten-year backtest results that
differ widely in return, because one keeps the companies that went bankrupt and the other does not, or because one
adjusts for splits and the other leaves a false 75% drop.

The useful question is not “which is the best provider?”, but “what does my system need and which defects can I not
afford?”. The order of work in this lesson is the one in the figure:

```mermaid
flowchart TB
  U[System use case] --> R[Requirements]
  R --> E{Knockout criteria met?}
  E -- no --> X[Discarded]
  E -- yes --> P[Weighted matrix]
  P --> M[Audit sample]
  M --> D[Document]
```

First you write down what the system does (asset, frequency, horizon, whether it trades live). The requirements follow
from that. Some are **knockout criteria**: if the provider does not meet them, the price does not matter. The rest are
weighted. And before paying, you test a sample, because a provider's website describes what it sells, not the defects it
has.

## The criteria, one by one

| Criterion | What to ask | Typical error if it fails |
|---|---|---|
| Coverage | Which markets, assets and venues? Consolidated tape or only the primary exchange? | Volumes that do not add up; missing assets (lesson 1.1.1) |
| Historical depth | Since when, and with what granularity in each period? | Backtest over a single market regime |
| Delisted securities | Does it include bankruptcies, mergers and removals, with their last price? | Survivorship bias (lesson 1.4.4) |
| Index members | Does it provide historical composition, with addition and removal dates? | Today's universe applied to the past |
| Corporate adjustments | Does it adjust for splits and dividends? By which method? Can you request unadjusted data? | False jumps; returns without dividends (lesson 1.4.3) |
| Identifiers | Is there a permanent identifier besides the ticker? | Histories cut by a ticker change |
| Point-in-time | Does it keep the versions of each data point with their publication date? | Look-ahead bias with fundamentals and macro (lessons 1.5.1 and 1.5.2) |
| Time zone and sessions | In which time zone do the timestamps come? Does it include extended hours? | Bars shifted by one hour twice a year |
| Latency and reliability | Real time, delayed, end of day? What happens if the API goes down? | Signals with yesterday's data in a live system |
| License | Personal or professional use? Can you publish results or derived data? | Breaching the contract (lesson 1.3.7) |
| Total cost | Subscription fee, usage cost, request limits and hours of cleaning | “Free” data that costs weeks of work |

Not all of them weigh the same in every system. For a daily equity study, delisted securities and adjustments are
knockout criteria and latency hardly matters. For an intraday futures system, dividend adjustments do not exist, but
the continuous series (lesson 1.4.6), latency and exact timestamps do.

## The course universe as a test bench

To measure biases you need to know the truth. The course universe is a fictional 40-stock index from 2015 to 2025 with
everything a cheap provider usually lacks: the prices of the 57 tickers that have been in the index (including the
bankrupt, the acquired and the removed ones) and the addition and removal dates of each member.

```python
import pandas as pd

precios = pd.read_parquet("universo_precios.parquet")
miembros = pd.read_csv("universo_miembros.csv",
                       parse_dates=["alta", "baja"])
eventos = pd.read_csv("universo_eventos.csv")

ancho = precios.pivot(index="fecha", columns="ticker",
                      values="cierre")
print(ancho.shape[1], "tickers,", len(ancho), "sessions")
eventos["evento"].value_counts()
```
```output
57 tickers, 2766 sessions
evento
exclusion             7
deslistado_quiebra    4
fusion                4
cambio_simbolo        2
Name: count, dtype: int64
```

With the addition and removal dates you build the membership mask: `True` if the security was a member on that day.
The portfolio for each session is decided with the members of the **previous** session (`shift(1)`), which is what was
known at the open.

```python
fechas = ancho.index
es_miembro = pd.DataFrame(False, index=fechas,
                          columns=ancho.columns)
for t, alta, baja in miembros.itertuples(index=False):
    fin = baja if pd.notna(baja) else pd.Timestamp.max
    es_miembro.loc[(fechas >= alta) & (fechas < fin), t] = True

peso = es_miembro.shift(1, fill_value=False)
rent = ancho.pct_change(fill_method=None)
es_miembro.sum(axis=1).unique()
```
```output
array([40])
```

Exactly 40 members every day, as the index rule says.

## Three providers, three returns

With this data you can simulate three providers that sell the “same” history:

- **Complete** (`completo`). It has the 57 tickers and the membership history. It is the truth of the universe.
- **Without delisted securities** (`sin_deslistadas`). It has the membership history, but only the tickers that trade
  today: the bankrupt, the acquired and the old tickers are missing. It is the most frequent defect in free sources.
- **Current members only** (`miembros_actuales`). Neither delisted securities nor membership history: the user takes
  today's 40 members and applies the whole history to them. It is what you do without noticing when you download “the
  index” from a current list.

```python
cotiza_hoy = ancho.columns[ancho.iloc[-1].notna()]
actuales = miembros.loc[miembros["baja"].isna(), "ticker"]

proveedores = {
    "completo": rent.where(peso),
    "sin_deslistadas": rent[cotiza_hoy].where(peso[cotiza_hoy]),
    "miembros_actuales": rent[actuales],
}
{k: v.shape[1] for k, v in proveedores.items()}
```
```output
{'completo': 57, 'sin_deslistadas': 47, 'miembros_actuales': 40}
```

The portfolio is equal-weighted and rebalanced every day: the index return is the average of the returns of the
members the provider has. If a member has no price, it simply does not enter the average, which is what any code that
does not check coverage would do.

```python
def anual(r_dia):
    curva = (1 + r_dia.iloc[1:].fillna(0)).cumprod()
    anos = (curva.index[-1] - curva.index[0]).days / 365.25
    return curva, curva.iloc[-1] ** (1 / anos) - 1

curvas, tabla = {}, {}
for nombre, r in proveedores.items():
    curvas[nombre], cagr = anual(r.mean(axis=1))
    tabla[nombre] = {"final": curvas[nombre].iloc[-1],
                     "anual_pct": 100 * cagr}
pd.DataFrame(tabla).T.round(2)
```
```output
                   final  anual_pct
completo            1.74       5.18
sin_deslistadas     2.21       7.49
miembros_actuales   3.13      10.94
```

The figure at the top compares the three providers. The same index, the same rule, the same eleven years. With the complete provider, 1 dollar becomes 1.74 (5.18% per
year). Without the delisted securities, 2.21 (7.49%). With the current members, 3.13 (10.94%): more than twice the
annual return. None of that is an edge of the system: it is the provider.

The reason is the one explained in lesson 1.4.4. Securities leave the index after falling (because of market
capitalization, because of bankruptcy) and enter it after rising. A provider without delisted securities erases the
losers of the past; using the current members also puts into the past the winners that were not there yet.

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

A stock selection system (momentum, value, quality) tested on a universe of survivors tends to look better than it is,
and the error is not visible in the code: it lies in what is missing from the data. For an equity study, delisted
securities and membership history are knockout criteria.

</aside>

It is not enough for the provider to say it has delisted securities. You have to check that it has the ones that matter
for your universe: the members of the index at the start of the history.

```python
ini = miembros.loc[miembros["alta"] == "2015-01-02", "ticker"]
for nombre, r in proveedores.items():
    tiene = ini.isin(r.columns).sum()
    print(f"{nombre:18} {tiene} of {len(ini)}")
```
```output
completo           40 of 40
sin_deslistadas    30 of 40
miembros_actuales  23 of 40
```

A current-members-only provider has 23 of the 40 securities the index started with: 42% of the 2015 universe has
disappeared from its data or was never there. The provider without delisted securities has 30: it is missing the ones
that went bankrupt, the acquired ones and the two old tickers.

## Identifiers: the ticker is not the company

Those two old tickers are another criterion. There are two ticker changes in the universe: ZXUV is renamed ZXMA in
2018 and ZXLU becomes ZXBI in 2022. They are the same company, but the data stores them as two series:

```python
cambios = eventos[eventos["evento"] == "cambio_simbolo"]
for _, ev in cambios.iterrows():
    for t in (ev["ticker"], ev["ticker_nuevo"]):
        s = ancho[t].dropna()
        print(f"{t}: {s.index[0]:%Y-%m-%d} to "
              f"{s.index[-1]:%Y-%m-%d}")
```
```output
ZXUV: 2015-01-02 to 2018-06-01
ZXMA: 2018-06-04 to 2025-12-31
ZXLU: 2015-01-02 to 2022-01-07
ZXBI: 2022-01-10 to 2025-12-31
```

A provider that indexes by ticker and does not link the two stretches gives ZXMA a history that starts in 2018: a
company that seems to have gone public that year. Serious providers maintain a **permanent identifier** (their own or
a public standard) that does not change with the ticker, and a ticker table with validity dates. When evaluating them,
ask for an example of a ticker change and see how they return it.

## Corporate adjustments: know what you are given

The second knockout criterion for equities is adjustments. A provider can give raw prices (the ones that were traded),
prices adjusted for splits, adjusted for splits and dividends, or all three. None of them is dangerous: what is
dangerous is not knowing which one it is. NOVA's daily series is **unadjusted**; if it is taken as if it were adjusted:

```python
nova = pd.read_csv("nova_diario_sin_ajustar.csv",
                   parse_dates=["fecha"], index_col="fecha")
r_nova = nova["cierre"].pct_change()
(100 * r_nova).nsmallest(3).round(2)
```
```output
fecha
2021-08-23   -75.19
2021-06-02   -12.21
2025-03-26   -12.10
Name: cierre, dtype: float64
```

A 75.19% drop in one day, with no bankruptcy or news. The audit consists of cross-checking the large jumps against the
corporate events:

```python
ev_nova = pd.read_csv("nova_eventos.csv", parse_dates=["fecha_ex"])

def saltos_con_evento(r, eventos, umbral=0.20):
    grandes = r[r.abs() > umbral]
    return pd.DataFrame({
        "rent_pct": (100 * grandes).round(2),
        "evento": grandes.index.isin(eventos["fecha_ex"]),
    })

saltos_con_evento(r_nova, ev_nova)
```
```output
            rent_pct  evento
fecha
2021-08-02     23.94   False
2021-08-23    -75.19    True
```

Of the two moves of more than 20%, only the one on 2021-08-23 falls on an event date: the 4-for-1 split. The other, a
23.94% rise on 2021-08-02, does not match any event: it is a market move and has to stay. A filter that deleted every
large jump would also remove that one, which is real. If the previous close is divided by the split ratio, the jump
disappears:

```python
dia = pd.Timestamp("2021-08-23")
previo = nova["cierre"].shift(1).loc[dia]
print(f"Previous close {previo:.2f}, close {nova.loc[dia, 'cierre']}")
print(f"Actual return: "
      f"{nova.loc[dia, 'cierre'] / (previo / 4) - 1:.2%}")
```
```output
Previous close 240.00, close 59.55
Actual return: -0.75%
```

The full adjustment (splits and dividends, multiplicative method) is lesson 1.4.3. To choose a provider it is enough to
know three things: whether it adjusts, by which method, and whether it also provides the raw prices and the event table
so you can redo the adjustment. A provider that only gives adjusted prices forces you to trust its adjustment; one that
gives raw prices and events lets you check it.

## Depth: regimes, not rows

Many free or cheap plans limit history to a few years. Two years of daily data are about 500 sessions, which seems like
a lot. The problem is that they may be a single type of market:

```python
pit = proveedores["completo"].mean(axis=1).iloc[1:]
por_ano = (1 + pit).groupby(pit.index.year).prod() - 1
(100 * por_ano).round(1).to_frame("rent_pct").T
```
```output
fecha     2015  2016  2017  2018  2019  2020  2021  2022  2023  2024  2025
rent_pct -24.6  18.7  10.5  -9.7  30.1  -9.7  49.3 -29.9  14.2  34.7   3.2
```

*Fig. — Annual return of the course index with the complete provider: a plan with two years of history only sees 2024 and 2025.* Vertical bars from 2015 to 2025: −24.6, +18.7, +10.5, −9.7, +30.1, −9.7, +49.3, −29.9, +14.2, +34.7 and +3.2%. An amber mark over 2024 and 2025 shows what a plan with two years of history would see: two positive years and no down year.

With only 2024 and 2025 you would see two positive years (+34.7% and +3.2%) and no bear year. The full history has
four negative years, among them 2015 (−24.6%) and 2022 (−29.9%). A system that has only been seen in two bull years has
not been tested in a downturn. For a daily system, useful depth is the one that covers several regimes: bull, bear,
sideways, low and high interest rates. For an intraday system, the number of sessions matters less than the variety of
conditions (news days, expirations, half-day sessions).

## The decision matrix

With clear criteria, the choice can be written in pandas. Three generic profiles, inspired by the categories in the
module (they are not specific providers and the scores are illustrative):

- **A**: unofficial free source, end of day, no delisted securities, personal license.
- **B**: paid retail API, delisted securities and adjustments, monthly fee.
- **C**: professional provider, broad coverage and tick data, high usage-based price.

Scores from 1 to 5 per criterion and two different use cases, each with its weights. First the knockout criteria, then
the weighted sum:

```python
notas = pd.DataFrame({
    "cobertura":    [3, 4, 5],
    "profundidad":  [3, 4, 5],
    "deslistadas":  [1, 4, 5],
    "ajustes":      [3, 4, 4],
    "intradia":     [1, 3, 5],
    "licencia":     [1, 3, 4],
    "coste":        [5, 4, 1],
}, index=["A", "B", "C"])

usos = pd.DataFrame({
    "diario_acciones":  [2, 3, 3, 3, 0, 1, 4],
    "intradia_futuros": [2, 2, 0, 0, 4, 1, 1],
}, index=notas.columns)
pesos = usos / usos.sum()

eliminatorio = {"diario_acciones": ("deslistadas", 3),
                "intradia_futuros": ("intradia", 4)}

def puntuar(uso):
    criterio, minimo = eliminatorio[uso]
    validos = notas[notas[criterio] >= minimo]
    return (validos @ pesos[uso]).round(2)

pd.DataFrame({u: puntuar(u) for u in usos})
```
```output
   diario_acciones  intradia_futuros
B             3.94               NaN
C             3.75               4.5
```

*Fig. — Decision matrix: weighted score of three provider profiles for two uses, after applying the knock-out criteria (schematic).* Horizontal bars. For a daily stock system, profile B scores 3.94 (highlighted, wins) and C 3.75; A is knocked out for lacking delisted stocks. For an intraday futures system, C scores 4.50 and A and B are knocked out by their intraday data score.

For the daily equity system, A is out before anything is added up: without delisted securities there is no valid
study, however cheap it is. Between B and C, B wins (3.94 versus 3.75): C is better at almost everything, but cost
weighs a lot and intraday does not count. For the intraday futures system, A and B are out because of the intraday data
criterion (`NaN` in the table) and C wins alone, with 4.5. With the
same providers, the choice changes with the use case; that is why the weights are written down before looking at the
prices, not after.

The matrix is not an algorithm for deciding but a way of making priorities explicit. If, after seeing the result, you
feel like changing the weights, it is worth asking whether the weight was wrong or whether you simply do not like the
result.

## Audit a sample before paying

Most providers let you try them: a free plan, a trial period or a sample. That sample goes through an automatic audit.
With the course universe as an example:

```python
def auditar(r_dia, universo_inicial, cotiza_hoy):
    tiene = r_dia.columns
    perdidos = sorted(set(universo_inicial) - set(tiene))
    muertos = set(tiene) - set(cotiza_hoy)
    return {
        "tickers": len(tiene),
        "faltan_del_inicio": len(perdidos),
        "deslistados_incluidos": len(muertos),
        "primer_dia": f"{r_dia.index[0]:%Y-%m-%d}",
        "saltos_75pct": int((r_dia.abs() > 0.70).sum().sum()),
    }

pd.DataFrame({k: auditar(v, ini, cotiza_hoy)
              for k, v in proveedores.items()})
```
```output
                         completo sin_deslistadas miembros_actuales
tickers                        57              47                40
faltan_del_inicio               0              10                17
deslistados_incluidos          10               0                 0
primer_dia             2015-01-02      2015-01-02        2015-01-02
saltos_75pct                    0               0                 0
```

Each row is a question asked of the sample: how many securities from the initial universe are missing, whether there
is any that no longer trades (if there is none, the database has no delisted securities; in the complete one there are
10, the 8 bankrupt or acquired ones and the 2 old tickers), when it starts, and whether there are impossible jumps. Here
no provider has 70% jumps because `universo_precios` already comes adjusted; the bankrupt ones fall to near zero over
several sessions, not all at once. The full list for any provider:

| Check | How |
|---|---|
| Delisted securities | Ask for 3 or 4 companies that went bankrupt or were acquired in your market and see whether they are there, up to their last day |
| Historical members | Ask for an index's composition on an old date and count it |
| Ticker changes | Ask for a security that changed its ticker and see whether the history is joined |
| Adjustments | Look for a known split in the prices and see whether there is a jump; ask for raw prices and events |
| Gaps | Compare the dates with the market calendar (lesson 1.1.2) |
| Time zone | Look at the time of the first intraday bar in winter and in summer |
| Volume | Compare one day's volume with another source: consolidated or only the primary exchange? |
| License | Read what can be published or shared before building anything on top (lesson 1.3.7) |

And one last recommendation: store, next to each dataset, which provider it comes from, with which parameters it was
requested and when. If a year from now the provider corrects its history, you will know why your backtest has changed
(lesson 1.4.8).

## Exercises

1. Repeat the annual return of the “without delisted securities” provider, also removing the securities that enter the
   index after 2015 (the IPOs and the replacements). Does it go up or down relative to 7.49%? Why?

**Solution.**

```python
viejos = [t for t in cotiza_hoy if t in set(ini)]
r_viejos = rent[viejos].where(peso[viejos])
_, cagr = anual(r_viejos.mean(axis=1))
print(len(viejos), "tickers;", f"{cagr:.2%} per year")
```
```output
30 tickers; 6.05% per year
```

Without the later additions, only the initial members that still trade remain. The return falls relative to the
provider without delisted securities, because the additions entered after rising strongly and kept contributing; but
it stays above the complete index, because the losers are missing. Every thing missing from the data moves the result,
and not always in the same direction.

2. In the decision matrix, raise the weight of cost in the `diario_acciones` use case from 4 to 8. Does the winner
   change? What happens if you also remove the knockout criterion?

**Solution.**

```python
usos.loc["coste", "diario_acciones"] = 8
pesos = usos / usos.sum()
print(puntuar("diario_acciones"))
print((notas @ pesos["diario_acciones"]).round(2))
```
```output
B    3.95
C    3.20
dtype: float64
A    3.40
B    3.95
C    3.20
dtype: float64
```

With cost at 8, B still wins (3.95) and C drops to 3.20. Without the knockout criterion, A, the provider without
delisted securities, scores 3.40 thanks to its price and ends up ahead of the professional one. That is exactly what the
prior filter prevents: a defect that invalidates the study is not offset by a discount.

3. Write the requirements and the knockout criteria of a system that trades a European ETF daily with closing data, and
   say which three audit checks you would run first.

**Solution.**

Requirements: daily closes from the European exchange where the ETF is listed, in its currency, with its holiday
calendar; enough history for several regimes; price adjusted for distributions if the ETF is a distributing one (or the
net asset value, lesson 1.2.2). Knockout criteria: that it has that ETF on its exchange (not only the US version), the
distributions, and a license that allows the intended use. Delisted securities weigh less than in an equity study
because the universe is a single product; they matter again if the system chooses among many ETFs, because ETFs also
get closed. The first three checks: dates against that exchange's calendar (lesson 1.1.2), distributions against the
fund manager's documents, and the time and time zone of the timestamps.

## Self-assessment

1. A provider only has the securities that trade today. With it you rebuild an equal-weighted index with its members on each day. What error do you make?

   - A. You overestimate the return: exactly the securities that went bankrupt or were acquired are missing, and they usually leave after falling
   - B. None, because you use the members of each day
   - C. You underestimate the return, because the IPOs are missing
   - D. It only affects volatility, not return

   *Answer A: You overestimate the return: exactly the securities that went bankrupt or were acquired are missing, and they usually leave after falling.* The member list is correct, but the ones that disappeared have no prices and the average is computed without them. In the course index, the annual return goes from 5.18% to 7.49%.

2. A daily series shows a 75.2% drop in one day, with no news. What is the most likely explanation?

   - A. A bankruptcy
   - B. A 4-for-1 split in unadjusted data
   - C. A rounding error
   - D. A special dividend

   *Answer B: A 4-for-1 split in unadjusted data.* A drop of almost exactly 1 − 1/4 matches a 4-for-1 split. If the series is not adjusted and the backtest takes it as a return, a short system would “earn” 75% that day. That is why jumps have to be cross-checked against corporate events.

3. Why does the decision matrix start with knockout criteria and not with the weighted sum?

   - A. Because it is faster
   - B. Because the weights are always equal
   - C. Because a high score on price or convenience does not make up for a defect that invalidates the result, such as having no delisted securities for an equity study
   - D. Because the weighted sum does not work with pandas

   *Answer C: Because a high score on price or convenience does not make up for a defect that invalidates the result, such as having no delisted securities for an equity study.* The weighted sum allows compensation: a lot of one criterion covers up too little of another. Some defects cannot be compensated; they are filtered out first.

4. A provider offers two years of history for free. What problem does it have for validating a daily system?

   - A. None, two years are 500 sessions
   - B. That free data is always wrong
   - C. That the format is different
   - D. That two years are usually a single market regime: in the course index, 2024 and 2025 give +34.7% and +3.2%, and there is no bear year like 2022 (−29.9%)

   *Answer D: That two years are usually a single market regime: in the course index, 2024 and 2025 give +34.7% and +3.2%, and there is no bear year like 2022 (−29.9%).* Depth is not measured in rows but in regimes. With two bull years you cannot know how the system behaves in a downturn.

5. A security changes its ticker in 2018. The provider indexes prices by the current ticker. What happens to its earlier history?

   - A. Nothing, the ticker does not matter
   - B. It becomes a delisted security
   - C. It depends: if the provider does not link the two tickers with a permanent identifier, the old history stays under the old ticker or is lost
   - D. It is adjusted like a split

   *Answer C: It depends: if the provider does not link the two tickers with a permanent identifier, the old history stays under the old ticker or is lost.* In the course universe, ZXUV becomes ZXMA on 2018-06-04: they are two separate series. A provider with a permanent identifier joins them; one that only knows the current ticker gives ZXMA a history that starts in 2018.

## Key points

- Choosing a provider is choosing which biases enter your results: backtest figures are only as good as the data.
- For equities, the knockout criteria are having delisted securities and membership history. In the course index, without them the annual return goes from 5.18% to 7.49% or to 10.94%.
- You need to know whether prices come adjusted, by which method, and whether they can be requested unadjusted. An unadjusted split is a false 75% drop.
- Useful depth is measured in market regimes, not in rows.
- A decision matrix starts with the knockout criteria and only then weights; the weights depend on the use case.
- Before paying, audit a sample: delisted securities, jumps against events, gaps, time zone and license.

## Resources

### Research

- [OpenBB Platform: free market data](https://pythonparatrading.com/en/openbb-free-market-data/) · Research-0014 · 10 min
- [Big Data for trading](https://pythonparatrading.com/en/big-data-for-trading/) · Research-0019 · 15 min

### Documentation

- [Survivorship bias and point-in-time data (course lesson 1.4.4)](https://pythonparatrading.com/en/python-trading-course/level-1/survivorship-bias-and-point-in-time-data/) · pythonparatrading.com

### Data

- [Level 1 data: prices of the fictional 40-stock universe (Parquet)](https://pythonparatrading.com/curso/datos/nivel-1/universo_precios.parquet) · Download · universo_precios.parquet
- [Level 1 data: index members with additions and removals (CSV)](https://pythonparatrading.com/curso/datos/nivel-1/universo_miembros.csv) · Download · universo_miembros.csv
- [Level 1 data: NOVA corporate events (CSV)](https://pythonparatrading.com/curso/datos/nivel-1/nova_eventos.csv) · Download · nova_eventos.csv

Previous lesson: [1.3.7 · Market data licensing and terms of use](https://pythonparatrading.com/en/python-trading-course/level-1/market-data-licensing-and-terms-of-use/)

Next lesson: [1.4.1 · Downloading over REST and websockets: pagination, rate limits and retries](https://pythonparatrading.com/en/python-trading-course/level-1/rest-websockets-pagination-and-retries/)
