---
title: "2.4.3 vectorbt: barridos de parámetros y carteras"
description: "vectorbt por dentro: broadcasting, Portfolio.from_signals y barridos masivos; el mismo barrido de 400 parejas de medias con NumPy, y su sobreajuste."
url: "https://pythonparatrading.com/curso-python-trading/nivel-2/vectorbt-barridos-de-parametros/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-2/vectorbt-barridos-de-parametros.png"
---

# 2.4.3 · vectorbt: barridos masivos de parámetros y simulación de carteras

> vectorbt convierte un barrido de miles de combinaciones de parámetros en unas pocas operaciones entre matrices. Esta lección explica cómo lo hace y su estado (licencia, edición PRO), programa el mismo barrido con NumPy sobre 24 futuros y 400 parejas de medias, y mide lo que casi nadie mide: cuánto vale fuera de muestra la mejor combinación.

- Curso: [Nivel 2: Estrategias sistemáticas y backtesting riguroso](https://pythonparatrading.com/curso-python-trading/nivel-2/)
- Módulo: 2.4 · Backtesting: motores y paradigmas
- Duración: 60 min
- Requisitos: [2.4.1 · Backtest vectorizado desde cero: matrices, desfase, costes y límites](https://pythonparatrading.com/curso-python-trading/nivel-2/backtest-vectorizado-desde-cero/), [0.5.1 · NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios](https://pythonparatrading.com/curso-python-trading/nivel-0/numpy-arrays-y-vectorizacion/)

## Al terminar esta lección

- Explicar cómo vectoriza vectorbt un barrido de parámetros (broadcasting de columnas, simulación compilada con Numba) y qué hacen `MA.run_combs` y `Portfolio.from_signals`.
- Conocer la licencia de vectorbt (Apache 2.0 con Commons Clause), qué prohíbe y qué ofrece la edición PRO, con fuentes.
- Programar con NumPy un barrido de 20 × 20 ventanas de cruce de medias sobre una cartera de futuros, con costes, y validarlo contra una implementación directa.
- Medir el sobreajuste de elegir la mejor combinación: Sharpe dentro y fuera de muestra, correlación de rangos y el mismo barrido sobre datos sin efecto.

## Qué es vectorbt y en qué estado está

vectorbt es una librería de Python para investigar estrategias a escala: indicadores, señales, simulación de carteras y
análisis de resultados sobre pandas y NumPy, con Numba (y un motor opcional en Rust) para la parte
pesada. Su especialidad es lo que en las lecciones 2.4.1 y 2.4.2 era caro: probar miles de combinaciones de
parámetros y de activos de una vez. A fecha de esta lección, su estado según sus fuentes primarias[^1]:

| Aspecto | Dato |
|---|---|
| Versión en PyPI | 1.2.0, publicada el 19 de octubre de 2026; Python ≥ 3.11 y < 3.15 |
| Repositorio | `polakowo/vectorbt` en GitHub, con actividad en octubre de 2026 |
| Licencia | Apache 2.0 con Commons Clause (`LICENSE.md`) |
| Edición comercial | VectorBT PRO, de pago por suscripción; repositorio, documentación y comunidad privados |
| Motor | Funciones compiladas con Numba; motor opcional en Rust (`vectorbt[rust]`) |

**La licencia pide leerla.** Apache 2.0 es una licencia permisiva; la **Commons Clause** le quita un derecho: el de
«vender» el software, definido como ofrecer a terceros, a cambio de una contraprestación, un producto o servicio cuyo
valor derive total o sustancialmente de su funcionalidad, «incluidos alojamiento o consultoría y soporte» relacionados.
Para un investigador o un trader que opera con su propio dinero no cambia nada. Para quien quiera montar un servicio de
backtesting de pago, una plataforma o una consultoría construida sobre vectorbt, sí: necesita otra licencia. Por eso,
y porque el titular del curso lo decidió así, en esta lección vectorbt no se ejecuta: su código aparece tal como lo
documenta la librería, y la parte ejecutable reproduce sus ideas con NumPy.

**La edición PRO.** El README presenta vectorbt como la edición comunitaria y de código abierto de VectorBT PRO, que
añade paralelización, más integraciones de datos, optimización de carteras, órdenes limitadas, apalancamiento y otras
funciones. Su página de comparación describe el acceso como una suscripción: repositorio privado, documentación y
Discord privados, y el derecho a seguir usando la versión instalada al terminar la suscripción. Las funciones de esta
lección son de la edición abierta.

## La idea: cada parámetro, una columna más

La lección 2.4.1 calculaba una cartera con una matriz `fecha × mercado`. vectorbt añade dimensiones: cada valor de
cada parámetro es una columna más, y la simulación trata todas las columnas a la vez. Un cruce de medias con 20
ventanas rápidas y 20 lentas sobre 24 mercados son $20 \times 20 \times 24 = 9.600$ columnas. Es **broadcasting**
(lección 0.5.1): NumPy combina un array de forma $(T, 20, 1)$ con otro de forma $(T, 1, 20)$ y obtiene $(T, 20, 20)$
sin copiar los datos de partida.

```mermaid
flowchart TB
  P[Precios T] --> R[Medias T×20]
  P --> L[Medias T×20]
  R --> S[Señales T×20×20]
  L --> S
  S --> W[Posiciones]
  W --> N[Rend. netos]
  N --> M[Sharpe 20×20]
```

El código de vectorbt para un barrido así, tomado de su README y adaptado a los futuros del curso, es este (no se
ejecuta en la lección):

```python skip
import numpy as np
import pandas as pd
import vectorbt as vbt

fut = pd.read_parquet("futuros2_continuos.parquet")
px = fut.pivot(index="fecha", columns="mercado",
               values="cierre_ajustado")

ventanas = np.arange(5, 401, 5)
rapida, lenta = vbt.MA.run_combs(px, window=ventanas, r=2,
                                 short_names=["rapida", "lenta"])
entradas = rapida.ma_crossed_above(lenta)
salidas = rapida.ma_crossed_below(lenta)

pf = vbt.Portfolio.from_signals(
    px, entradas, salidas,
    short_entries=salidas, short_exits=entradas,  # largo y corto
    fees=0.0005, slippage=0.0005, freq="1D")
sharpe = pf.sharpe_ratio()          # una cifra por columna
sharpe.groupby(["rapida_window", "lenta_window"]).mean()
```

`MA.run_combs` calcula las medias de todas las ventanas y forma las combinaciones de dos (`r=2`), con la primera
ventana menor que la segunda; el resultado tiene un nivel de columnas por parámetro (`rapida_window`, `lenta_window`) y
otro por mercado. `Portfolio.from_signals` simula la cartera de cada columna: compra en las entradas, vende en las
salidas, cobra comisiones (`fees`, en fracción del valor de la orden) y deslizamiento (`slippage`, en fracción del
precio). Por defecto la orden se ejecuta al precio de cierre de la barra de la señal, la misma hipótesis que el
`shift(1)` de la lección 2.4.1.

El recorrido completo, de los datos a las métricas:

```mermaid
flowchart TB
  D[Datos: columnas] --> I[MA.run_combs]
  I --> S[Entradas, salidas]
  S --> P[from_signals]
  P --> N[Bucle Numba]
  N --> R[Órdenes y trades]
  R --> M[stats, heatmap]
```

Lo que no se ve: según su documentación, el módulo de cartera usa funciones compiladas con Numba para casi todos los
cálculos. La simulación **sí recorre el tiempo**, barra a barra, pero en código compilado y sobre todas las columnas.
Por eso `from_signals` admite reglas con memoria que un cálculo puramente matricial no puede hacer:

```python skip
# Trailing stop del 4 % con máximos y mínimos de la barra
pf = vbt.Portfolio.from_signals(
    close, entradas, salidas,
    open=apertura, high=maximo, low=minimo,
    sl_stop=0.04, sl_trail=True)

pf.stats()                          # resumen de una columna
sharpe.vbt.heatmap(x_level="lenta_window", y_level="rapida_window",
                   slider_level="mercado")
```

`sl_stop` es el stop en fracción del precio de entrada y `sl_trail=True` lo convierte en trailing; `open`, `high` y
`low` solo se usan para decidir si un stop se ha tocado dentro de la barra. Es un motor por eventos en miniatura
(lección 2.4.2), con sus propias reglas para el orden de las cosas dentro de una barra: antes de confiar en él para una
estrategia intradía hay que leerlas.

## El mismo barrido con NumPy

La parte ejecutable reproduce el barrido con lo que ya sabemos. Datos, volatilidad y costes son los de la lección
2.4.1: 24 futuros sintéticos con un momentum de 12 meses y un carry plantados, posiciones con objetivo de volatilidad y
costes por contrato sobre la rotación y los rolls.

```python
import numpy as np
import pandas as pd

fut = pd.read_parquet("futuros2_continuos.parquet")
spec = pd.read_csv("futuros2_especificaciones.csv",
                   index_col="mercado")
tabla = {c: fut.pivot(index="fecha", columns="mercado", values=c)
         for c in ("cierre_ajustado", "cierre", "contrato")}
px, bruto, contrato = tabla.values()
r = px.pct_change(fill_method=None).fillna(0.0)
coste_usd = (spec["coste_tipico_ticks"] * spec["valor_tick"]
             + spec["comision_usd"])
coste_unit = (coste_usd / (bruto * spec["multiplicador"])).fillna(0.0)
rolls = (contrato != contrato.shift(1)) & contrato.shift(1).notna()
n = px.shape[1]
vol = r.ewm(span=60, min_periods=60).std() * np.sqrt(252)
k = (0.10 / np.sqrt(n) / vol).fillna(0.0)   # peso por unidad de señal
print(px.shape, "| riesgo por mercado:", round(0.10 / np.sqrt(n), 4))
```
```output
(6539, 24) | riesgo por mercado: 0.0204
```

La regla: estar largo si la media rápida está por encima de la lenta y corto si está por debajo. Las ventanas forman
una rejilla geométrica de 20 valores cada una: rápidas de 5 a 120 sesiones y lentas de 20 a 400. Las parejas con la
rápida igual o mayor que la lenta no tienen sentido y se descartan.

```python
rapidas = np.unique(np.geomspace(5, 120, 20).round().astype(int))
lentas = np.unique(np.geomspace(20, 400, 20).round().astype(int))
valido = rapidas[:, None] < lentas[None, :]   # broadcasting 20 x 20
print("rápidas:", rapidas)
print("lentas: ", lentas)
print(f"combinaciones: {valido.size}, válidas: {valido.sum()}")
```
```output
rápidas: [  5   6   7   8  10  12  14  16  19  23  27  31  37  44  52  61  73  86
 102 120]
lentas:  [ 20  23  27  32  38  44  52  60  71  83  97 113 133 155 182 213 249 292
 342 400]
combinaciones: 400, válidas: 325
```

Las medias de todas las ventanas salen de una sola suma acumulada: la media de $w$ sesiones que termina en $t$ es
$(C_t - C_{t-w}) / w$, con $C_t$ la suma de los precios hasta $t$. Antes de usarla, se comprueba contra `rolling`:

```python
def medias(p, ventanas):
    """Media móvil simple de p para cada ventana (una columna por
    ventana), con una suma acumulada en lugar de un rolling."""
    cs = np.concatenate([[0.0], np.cumsum(p)])
    out = np.full((len(p), len(ventanas)), np.nan)
    for j, w in enumerate(ventanas):
        out[w - 1:, j] = (cs[w:] - cs[:-w]) / w
    return out

ref = px["IDA"].rolling(97).mean().to_numpy()
error = np.nanmax(np.abs(medias(px["IDA"].to_numpy(), [97])[:, 0]
                         - ref))
print(f"error máximo frente a rolling: {error:.1e} puntos")
```
```output
error máximo frente a rolling: 1.5e-10 puntos
```

El barrido entero es una función. La línea importante es la de la señal: `mf[:, :, None]` tiene forma $(T, 20, 1)$ y
`ms[:, None, :]`, $(T, 1, 20)$; su diferencia es el tensor $(T, 20, 20)$ con todas las parejas. El tamaño de cada
mercado, `k`, tiene forma $(T,)$ y se amplía con `[:, None, None]`. El bucle es sobre los 24 mercados, no sobre las
400 parejas ni sobre las 6.539 sesiones.

```python
def barrido(px, r, k, coste_unit, rolls, rapidas, lentas,
            largo_corto=True):
    """Rendimiento neto diario de la cartera para cada pareja de
    ventanas: array de forma (fechas, rápidas, lentas)."""
    T, F, S = len(px), len(rapidas), len(lentas)
    neto = np.zeros((T, F, S))
    for m in px.columns:
        p = px[m].to_numpy()
        mf, ms = medias(p, rapidas), medias(p, lentas)
        dif = mf[:, :, None] - ms[:, None, :]           # T x F x S
        senal = np.sign(dif) if largo_corto else (dif > 0) * 1.0
        w = np.nan_to_num(senal) * k[m].to_numpy()[:, None, None]
        pos = np.concatenate([np.zeros((1, F, S)), w[:-1]])
        rot = (np.abs(w - pos)
               + 2 * np.abs(pos) * rolls[m].to_numpy()[:, None, None])
        neto += (pos * r[m].to_numpy()[:, None, None]
                 - rot * coste_unit[m].to_numpy()[:, None, None])
    return neto

neto = barrido(px, r, k, coste_unit, rolls, rapidas, lentas)
print(neto.shape, f"{neto.nbytes / 1e6:.0f} MB")
```
```output
(6539, 20, 20) 21 MB
```

Un array de 6.539 × 20 × 20 números, 21 MB. vectorbt haría lo mismo con más columnas (también los 24 mercados por
separado) y guardaría además las órdenes y las operaciones de cada una. Un barrido de 80 ventanas por 80 sobre 24
mercados ya serían 150.000 columnas: la memoria, más que el tiempo, es el límite de esta forma de trabajar.

Un barrido rápido que da una respuesta equivocada no sirve de nada, así que una celda del tensor se compara con una
implementación directa, escrita como en la lección 2.4.1:

```python
def una_pareja(f, s):
    dif = px.rolling(f).mean() - px.rolling(s).mean()
    w = np.sign(dif).fillna(0.0) * k
    pos = w.shift(1, fill_value=0.0)
    rot = (w - pos).abs() + 2 * pos.abs().where(rolls, 0.0)
    return (pos * r).sum(axis=1) - (rot * coste_unit).sum(axis=1)

i, j = list(rapidas).index(31), list(lentas).index(97)
dif = np.abs(una_pareja(31, 97).to_numpy() - neto[:, i, j]).max()
print(f"pareja (31, 97): diferencia máxima {dif:.1e}")
```
```output
pareja (31, 97): diferencia máxima 2.8e-17
```

## El mapa de Sharpe: meseta y picos

```python
fechas = px.index
def sharpe(x):
    return np.sqrt(252) * x.mean(axis=0) / x.std(axis=0, ddof=1)

def mapa(neto, desde, hasta=None):
    filas = (fechas >= desde) & (fechas < (hasta or "2100"))
    sh = np.full(valido.shape, np.nan)
    sh[valido] = sharpe(neto[filas][:, valido])
    return sh

sh = mapa(neto, "2001")
i, j = np.unravel_index(np.nanargmax(sh), sh.shape)
print(f"mejor pareja: ({rapidas[i]}, {lentas[j]}), "
      f"Sharpe {sh[i, j]:.2f}")
print(f"mediana de las {valido.sum()}: {np.nanmedian(sh):.2f}; "
      f"por encima de 0,45: {(sh > 0.45).sum()}")
pd.DataFrame(sh, index=rapidas, columns=lentas).iloc[::3, ::3].round(2)
```
```output
mejor pareja: (27, 342), Sharpe 1.00
mediana de las 325: 0.55; por encima de 0,45: 202
      20    32    52    83    133   213   342
5    0.06  0.23  0.28  0.37  0.55  0.68  0.92
8    0.09  0.24  0.31  0.42  0.55  0.68  0.90
14   0.09  0.20  0.30  0.41  0.54  0.67  0.95
23    NaN  0.27  0.34  0.41  0.68  0.70  0.97
37    NaN   NaN  0.30  0.49  0.71  0.82  0.93
61    NaN   NaN   NaN  0.65  0.73  0.71  0.86
102   NaN   NaN   NaN   NaN  0.46  0.46  0.72
```

La figura de la cabecera es el mapa completo. El Sharpe neto crece de forma casi continua con la media lenta: en la
tabla, con una lenta de 20 sesiones va de 0,06 a 0,09; con una de 342, de 0,72 a 0,97. La zona buena no es un punto:
es una **meseta** amplia de medias lentas largas, y 202 de las 325 parejas superan 0,45 (la mediana es 0,55). Tiene
sentido: el efecto plantado en estos futuros es el signo del rendimiento de las 252 sesiones anteriores, y cualquier
cruce de medias lento lo aproxima. Las medias lentas cortas siguen tendencias de semanas, que en estos datos no tienen
prima, rotan mucho y pagan más costes. La mejor celda, (27, 342) con 1,00, está en esa meseta, rodeada de vecinas casi
igual de buenas (en la tabla, 0,97 con (23, 342) y 0,93 con (37, 342)), y eso es lo que la hace creíble; un máximo
aislado, rodeado de celdas mediocres, sería lo contrario (lección 2.5.6).

Pero el mapa entero está calculado sobre toda la muestra. La pregunta honesta es la que se haría en tiempo real: si
hubiera elegido la mejor pareja con los datos de 2001 a 2012, ¿qué habría obtenido de 2013 a 2025?

```python
sh_in = mapa(neto, "2001", "2013")[valido]
sh_out = mapa(neto, "2013")[valido]
pares = np.argwhere(valido)            # mismo orden que [valido]
b = sh_in.argmax()
f, s = rapidas[pares[b, 0]], lentas[pares[b, 1]]
print(f"mejor dentro de muestra: ({f}, {s}), Sharpe {sh_in[b]:.2f}")
print(f"la misma, fuera de muestra: {sh_out[b]:.2f}")
print(f"mediana fuera de muestra: {np.median(sh_out):.2f}")
rho = pd.Series(sh_in).rank().corr(pd.Series(sh_out).rank())
print(f"correlación de rangos dentro/fuera: {rho:.2f}")
quintil = pd.qcut(sh_in, 5, labels=["Q1", "Q2", "Q3", "Q4", "Q5"])
pd.Series(sh_out).groupby(quintil, observed=True).mean().round(2)
```
```output
mejor dentro de muestra: (27, 155), Sharpe 0.79
la misma, fuera de muestra: 0.70
mediana fuera de muestra: 0.63
correlación de rangos dentro/fuera: 0.72
Q1    0.20
Q2    0.50
Q3    0.58
Q4    0.85
Q5    0.92
dtype: float64
```

*Fig. — Barrido de 325 parejas de medias sobre 24 futuros sintéticos: Sharpe medio fuera de muestra (2013–2025) según el quintil de Sharpe dentro de muestra (2001–2012), y lo que da fuera la mejor pareja de dentro.* Cinco barras que crecen con el quintil: Sharpe medio fuera de muestra de cada quintil de parejas ordenadas por su Sharpe dentro de muestra, 0,20, 0,50, 0,58, 0,85 y 0,92 del peor al mejor quintil. Una línea destacada a 0,70 marca lo que da fuera la mejor pareja de dentro, que tenía un Sharpe de 0,79: por debajo de la media de su propio quintil.

Dos resultados a la vez, y los dos importan. El primero: el barrido ha encontrado algo real. El orden de las parejas
se conserva de un periodo al otro (correlación de rangos de 0,72) y, agrupadas por quintil de su Sharpe dentro de
muestra, las parejas rinden fuera de menos a más, de 0,20 en el peor quintil a 0,92 en el mejor. Es lo que se espera
cuando el mapa tiene una estructura de verdad: las medias lentas recogen el momentum plantado y pagan menos costes en
cualquier periodo.

El segundo: la mejor pareja de 2001-2012, (27, 155), no es la mejor de 2013-2025. Tenía un Sharpe de 0,79, 0,31 por
encima de la mediana de su periodo (0,48, en la celda siguiente); fuera da 0,70, solo 0,07 por encima de la mediana
(0,63) y por debajo de la media de su propio quintil. Toda la meseta rinde más en el segundo periodo, porque las
tendencias de esos años fueron más rentables; lo que no se repite es la distancia del máximo a sus vecinas. Esa parte
del Sharpe era ruido de 2001-2012, y es justo la que se paga al elegir el máximo. Quedarse con la zona buena (el
quintil superior entero da 0,92 fuera) habría dado más que quedarse con la mejor celda.

## Un barrido sobre datos sin efecto

La última comprobación es la más dura: repetir el barrido sobre datos en los que **no hay nada que encontrar**. Si se
permutan al azar los rendimientos diarios de cada mercado, se conserva su distribución (volatilidad, colas) y se
destruye cualquier tendencia. El mejor resultado de 325 combinaciones sobre esos datos mide cuánto «encuentra» la
búsqueda por pura suerte.

```python
rng = np.random.default_rng(2028)
r_azar = r.apply(lambda c: pd.Series(rng.permutation(c.to_numpy()),
                                     c.index))
px_azar = (1 + r_azar).cumprod()
vol_azar = r_azar.ewm(span=60, min_periods=60).std() * np.sqrt(252)
k_azar = (0.10 / np.sqrt(n) / vol_azar).fillna(0.0)
neto_azar = barrido(px_azar, r_azar, k_azar, coste_unit, rolls,
                    rapidas, lentas)
for nombre, x in [("datos sintéticos", neto),
                  ("permutados", neto_azar)]:
    s_in = mapa(x, "2001", "2013")[valido]
    print(f"{nombre:16s} 2001-2012: mediana {np.median(s_in):5.2f}, "
          f"máximo {s_in.max():.2f}")
```
```output
datos sintéticos 2001-2012: mediana  0.48, máximo 0.79
permutados       2001-2012: mediana -0.46, máximo 0.15
```

*Fig. — Distribución del Sharpe neto de 2001 a 2012 de las 325 parejas de medias: sobre rendimientos permutados, sin ningún efecto (arriba), y sobre los futuros sintéticos con el momentum plantado (abajo).* Dos histogramas con el mismo eje. Arriba, datos permutados: la mayoría de parejas entre −0,8 y 0, mediana −0,46, y una línea destacada en el máximo, 0,15. Abajo, datos sintéticos: la mayoría entre 0,2 y 0,8, mediana 0,48, y una cola que llega a 0,79; 312 de las 325 parejas quedan a la derecha del máximo del azar.

Sobre datos permutados, la mediana del Sharpe es de −0,46 (sin tendencias, lo único que queda son los costes) y el
máximo de 325 combinaciones llega a 0,15 en doce años sin ningún efecto. Es la referencia contra la que hay que medir
un barrido: el mejor resultado de los datos sintéticos, 0,79, está muy por encima, y su mediana (0,48) también supera
al máximo del azar; en la figura, casi todas las parejas quedan a su derecha. El efecto plantado es modesto (un Sharpe
de en torno a 0,15 por mercado), pero sumado en 24 mercados se separa con claridad del azar. Una estrategia real
cuyo mejor Sharpe en un barrido así no se separara del máximo de las permutaciones no habría demostrado nada.
Y este azar es benévolo: permutar los días también destruye el agrupamiento de la volatilidad y las rachas, y los
mercados reales dan al ruido más formas de parecer una tendencia. La lección 2.5.4 convierte esa intuición
en una corrección formal (el Deflated Sharpe Ratio, que vectorbt también calcula con `deflated_sharpe_ratio()`), y la
2.5.5 generaliza las permutaciones.

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

Un barrido de miles de combinaciones es una máquina de producir falsos positivos. Registra cuántas pruebas has hecho
(lección 2.2.4), elige por mesetas y no por máximos (lección 2.5.6), deja datos fuera de muestra sin tocar (lección
2.5.2) y corrige el Sharpe por el número de pruebas (lección 2.5.4). Y recuerda que estos datos son sintéticos con un
momentum plantado: la meseta existe porque la pusimos ahí.

</aside>

## Cuándo usar vectorbt, y cuándo no

| Situación | vectorbt | Alternativa |
|---|---|---|
| Barrer miles de parámetros sobre muchos activos | Su terreno: columnas y Numba | NumPy propio, como en esta lección |
| Explorar señales e indicadores con gráficos | Muy cómodo (Plotly, `heatmap`, `stats`) | pandas y matplotlib |
| Stops y trailing stops sobre barras | Sí, con sus reglas de orden dentro de la barra | Motor por eventos propio (2.4.2) |
| Órdenes limitadas, apalancamiento, multiplicadores de futuros | Según su README, en la edición PRO | NautilusTrader (2.4.5) |
| Mismo código en backtest y en real | No es su objetivo | NautilusTrader, LEAN (2.4.5, 2.4.6) |
| Vender un servicio basado en él | No sin otra licencia (Commons Clause) | Librerías con licencia permisiva o propias |

La decisión práctica: vectorbt es una herramienta excelente para la fase de exploración, la misma que en la lección
2.4.1 hacía el backtest vectorizado propio, con más velocidad y más análisis ya hechos. La validación final de una
estrategia que sobreviva al barrido pertenece a un motor por eventos con las reglas exactas, y la decisión de cuántas
pruebas hacer, a ti.

[^1]: PyPI (`pypi.org/project/vectorbt`, metadatos de la versión 1.2.0), el repositorio de GitHub (`README.md` y
    `LICENSE.md`) y la página «VectorBT vs PRO» de su documentación, consultados el 21 de octubre de 2026. No se
    citan precios de la edición PRO: cambian y no son el objeto de la lección.

## Ejercicios

1. Repite el barrido solo con posiciones largas (`largo_corto=False`): largo si la rápida está por encima, fuera si no.
   ¿Cambia la mediana del mapa? ¿Y la posición de la mejor pareja?

**Solución.**

```python
neto_l = barrido(px, r, k, coste_unit, rolls, rapidas, lentas,
                 largo_corto=False)
sh_l = mapa(neto_l, "2001")
i, j = np.unravel_index(np.nanargmax(sh_l), sh_l.shape)
print(f"solo largos: mediana {np.nanmedian(sh_l):.2f}, mejor "
      f"({rapidas[i]}, {lentas[j]}) con {sh_l[i, j]:.2f}")
print(f"largo y corto: mediana {np.nanmedian(sh):.2f}")
```
```output
solo largos: mediana 0.84, mejor (27, 342) con 1.11
largo y corto: mediana 0.55
```

Solo con largos la mediana sube de 0,55 a 0,84 y la mejor pareja sigue siendo (27, 342), ahora con 1,11. El momentum
plantado es simétrico, pero no es lo único que hay en estos datos: el README del nivel 2 añade a los futuros de índices
la prima de la renta variable (la deriva del guion), y la parte corta del cruce la paga cada vez que está vendida;
además, estar fuera en lugar de corto reduce la exposición, la rotación y los costes. Es un buen ejemplo de por qué un
resultado de barrido se explica antes de usarlo: la mejora no viene de la regla de tendencia, sino de una prima que la
versión larga recoge y la larga-corta neutraliza. En otro universo, la comparación saldría distinta.

2. Cuenta cuántas parejas tendrían un estadístico $t$ mayor que 2 en 2001-2012 (con $t \approx S \sqrt{N}$, $S$
   el Sharpe anual y $N$ los años) en los datos sintéticos y en los permutados. ¿Qué dice el resultado de los permutados sobre un
   umbral fijo de $t > 2$?

**Solución.**

```python
anos = 12
for nombre, x in [("sintéticos", neto), ("permutados", neto_azar)]:
    t = mapa(x, "2001", "2013")[valido] * np.sqrt(anos)
    print(f"{nombre:10s} t > 2: {(t > 2).sum():3d} de {t.size}")
```
```output
sintéticos t > 2:  99 de 325
permutados t > 2:   0 de 325
```

En los sintéticos, 99 de 325 parejas superan $t > 2$ porque el efecto existe; en los permutados, ninguna, en parte
porque los costes hunden la mediana. Las 99 no son 99 descubrimientos: son el mismo momentum visto a través de
ventanas muy parecidas, con rendimientos muy correlacionados entre sí. Y un umbral fijo no es una garantía: con pruebas
independientes y sin costes, $t > 2$ dejaría pasar por azar en torno a un 2 % de ellas (unas 7 de 325), y en datos
reales el ruido tiene más formas de parecer una tendencia que en una permutación. El umbral correcto depende del
número de pruebas y de lo correlacionadas que estén, y la cuenta tampoco dice cuál de las 99 elegir. Las lecciones 2.1.8 y 2.5.4 tratan el
ajuste por pruebas múltiples.

3. vectorbt ejecuta por defecto al cierre de la barra de la señal y `from_signals` no deja entrar de nuevo mientras ya
   hay posición. ¿Qué dos diferencias de resultado esperarías entre el código `skip` de vectorbt de esta lección y el
   barrido con NumPy, aun con los mismos datos y ventanas?

**Solución.**

La primera es el dimensionado: el barrido con NumPy usa pesos con objetivo de volatilidad que cambian cada día, y el
ejemplo de vectorbt invierte todo el efectivo disponible en cada entrada (`size` por defecto infinito), sin
rebalancear entre señales; ni el riesgo ni la rotación son comparables. La segunda son los costes: allí, un porcentaje
fijo del valor negociado; aquí, un coste por contrato que depende del tamaño del contrato, más los rolls, que vectorbt
no ve en un precio ajustado. Para que coincidieran habría que pasarle pesos objetivo (`Portfolio.from_orders` con
`size_type` de tipo objetivo) y un coste equivalente: la misma reconciliación de la lección 2.4.2.

## Autoevaluación

1. ¿Qué permite y qué no permite la Commons Clause que acompaña a la licencia Apache 2.0 de vectorbt?

   - A. Prohíbe usarlo con fines comerciales, incluido operar con dinero propio
   - B. Permite usar, modificar y redistribuir el código, pero no vender un producto o servicio cuyo valor derive total o sustancialmente de vectorbt, incluidos alojamiento o consultoría sobre él
   - C. Obliga a publicar el código de las estrategias
   - D. Es una licencia de dominio público

   *Respuesta B: Permite usar, modificar y redistribuir el código, pero no vender un producto o servicio cuyo valor derive total o sustancialmente de vectorbt, incluidos alojamiento o consultoría sobre él.* El texto de la condición, en el `LICENSE.md` del repositorio, quita del permiso de Apache 2.0 el derecho a «vender» el software, y define vender como ofrecer a terceros, a cambio de una contraprestación, un producto o servicio cuyo valor derive sustancialmente de su funcionalidad. Usarlo para investigar o para operar no es venderlo; montar un servicio de backtesting de pago sobre él, sí.

2. En el barrido de la lección, la combinación con mejor Sharpe de 2001 a 2012 (rápida 27, lenta 155) tiene 0,79 dentro de muestra, 0,31 por encima de la mediana (0,48). ¿Qué ocurre de 2013 a 2025?

   - A. Mantiene su ventaja: el efecto plantado es real
   - B. Da 0,70, solo 0,07 por encima de la mediana fuera de muestra (0,63) y por debajo de la media de su propio quintil (0,92)
   - C. Su Sharpe se vuelve negativo
   - D. No se puede saber sin vectorbt

   *Respuesta B: Da 0,70, solo 0,07 por encima de la mediana fuera de muestra (0,63) y por debajo de la media de su propio quintil (0,92).* El barrido encontró algo real: el orden de las parejas se conserva fuera de muestra (correlación de rangos de 0,72, quintiles crecientes). Pero la parte del Sharpe que separaba a la mejor de las demás era sobre todo ruido de ese periodo: su ventaja sobre la mediana pasa de 0,31 a 0,07. Elegir el máximo no compensa; elegir la zona buena, sí.

3. ¿Por qué el mismo barrido sobre rendimientos permutados (sin ningún efecto) es una comprobación útil?

   - A. Porque así el barrido tarda menos
   - B. Porque da la distribución del mejor resultado cuando no hay nada que encontrar: si el máximo de los datos reales no se separa de él, el barrido no ha encontrado una ventaja
   - C. Porque elimina los costes
   - D. Porque mejora el Sharpe

   *Respuesta B: Porque da la distribución del mejor resultado cuando no hay nada que encontrar: si el máximo de los datos reales no se separa de él, el barrido no ha encontrado una ventaja.* Permutar los rendimientos de cada mercado destruye cualquier tendencia y conserva su distribución. El mejor de 325 resultados sobre esos datos mide cuánto puede «encontrar» la búsqueda por azar. Es la idea de las pruebas de permutación de la lección 2.5.5.

4. `Portfolio.from_signals` admite `sl_stop` con `sl_trail=True`. ¿Cómo puede un backtester «vectorizado» simular un trailing stop?

   - A. No puede: el parámetro no existe
   - B. Porque su simulación es un bucle sobre el tiempo compilado con Numba, que recorre cada columna barra a barra; lo vectorizado es el paso de muchas columnas a la vez
   - C. Porque precalcula todos los máximos con `rolling`
   - D. Porque usa la GPU

   *Respuesta B: Porque su simulación es un bucle sobre el tiempo compilado con Numba, que recorre cada columna barra a barra; lo vectorizado es el paso de muchas columnas a la vez.* La documentación de vectorbt dice que el módulo de cartera usa funciones compiladas con Numba para casi todos los cálculos. El recorrido en el tiempo existe, pero en código compilado y sobre miles de columnas a la vez: por eso puede llevar estado (posición, máximo, stop) sin la lentitud de un bucle de Python.

## Ideas clave

- vectorbt amplía cada parámetro a una columna más: 400 parejas de medias sobre 24 mercados son 9.600 columnas que se simulan juntas.
- Su licencia es Apache 2.0 con Commons Clause: se puede usar y modificar, pero no vender un producto o servicio basado en él. VectorBT PRO es una edición de pago por suscripción, con repositorio y documentación privados.
- El mismo barrido se programa con NumPy: medias con sumas acumuladas, señales con broadcasting `fecha × rápida × lenta` y costes sobre la rotación; 325 combinaciones válidas en segundos.
- Con un efecto real, el orden del mapa se conserva fuera de muestra (correlación de rangos 0,72), pero el máximo se encoge: la mejor pareja de 2001-2012 pasa de 0,79 a 0,70, y su ventaja sobre la mediana, de 0,31 a 0,07.
- En los datos sintéticos, la zona buena del mapa es una meseta amplia de medias lentas, coherente con el momentum de 12 meses plantado; sobre datos permutados, el máximo de 325 combinaciones se queda en 0,15 dentro de muestra.
- Que muchas parejas pasen un umbral (99 de 325 con t > 2) no son muchos descubrimientos: es el mismo efecto visto a través de ventanas muy correlacionadas.

## Recursos

### Research

- [Estacionalidad Intradiaria IV - Busqueda de campo](https://pythonparatrading.com/estacionalidad-intradia-optimizacion/) · Research-0027 · 7 min

### Documentación

- [vectorbt en GitHub (README y LICENSE.md con la Commons Clause)](https://github.com/polakowo/vectorbt) · github.com
- [vectorbt en PyPI (versión 1.2.0, 19 de octubre de 2026)](https://pypi.org/project/vectorbt/) · pypi.org
- [vectorbt: documentación de Portfolio (from_signals, broadcasting, Numba)](https://vectorbt.dev/api/portfolio/base/) · vectorbt.dev
- [VectorBT vs PRO: diferencias y acceso](https://vectorbt.dev/getting-started/upgrade/) · vectorbt.dev
- [NumPy: broadcasting](https://numpy.org/doc/stable/user/basics.broadcasting.html) · numpy.org

### Datos

- [Datos del nivel 2: futuros continuos sintéticos (Parquet)](https://pythonparatrading.com/curso/datos/nivel-2/futuros2_continuos.parquet) · Descarga · futuros2_continuos.parquet

Lección anterior: [2.4.2 · Backtest por eventos desde cero: bucle, cartera, órdenes y fills](https://pythonparatrading.com/curso-python-trading/nivel-2/backtest-por-eventos-desde-cero/)

Lección siguiente: [2.4.4 · backtesting.py y backtrader: dos motores didácticos y su estado](https://pythonparatrading.com/curso-python-trading/nivel-2/backtesting-py-y-backtrader/)
