Un simulador que dibuja 10.000 futuros posibles de una acción y luego se examina a sí mismo: ¿sus «90%» se cumplen el 90% de las veces? Con SPY, el modelo de libro dice 50% y acierta el 73%.
Entra una cinta con 20 años de precios, dos motores la convierten en 10.000 futuros posibles y una lupa comprueba si esos futuros dijeron la verdad.
Escribes «Apple». El programa baja 20 años de precios de Yahoo y mide cuánto se mueve la acción. Con eso fabrica 10.000 futuros a 3 meses de dos maneras distintas.
Lo que lo hace distinto de los cientos de repos iguales: al final se examina a sí mismo contra 15 años de fechas reales.
| En el dibujo | Qué hace | Dónde |
|---|---|---|
| cinta de precios | baja el histórico ajustado y elige la bolsa que más mueve (resolución por nombre) | montecarlo.py:99 |
| engranaje GBM | inventa días con una campana normal (movimiento browniano geométrico) | montecarlo.py:48 |
| engranaje bloques | copia meses reales del pasado en otro orden (bootstrap por bloques) | montecarlo.py:52 |
| abanico | 10.000 caminos → franjas, VaR, Expected Shortfall | montecarlo.py:147 |
| lupa | el examen: ¿se cumplen sus «90%»? (backtest de calibración) | montecarlo.py:182 |
| ventanilla | servidor sin dependencias que entrega todo en un JSON | server.py:31 |
Dato real: Apple a 3 meses desde 333,69 USD: 6 de cada 10 futuros acaban en verde (64,0% con bootstrap, 62,1% con GBM).
El bichito baja una campana de cristal (la distribución normal) sobre un montón de días reales: los tranquilos caben, pero los días extremos se quedan fuera, en rojo.
Si los días de bolsa siguieran una campana, un día de más de 4 desviaciones (±4σ) pasaría unas 0,3 veces en 20 años de SPY. Pasó 38 veces.
Y el centro es más estrecho de lo que dice la campana: muchos días aburridos y unos pocos brutales. Eso se mide con la curtosis: 0 en una campana y 14,1 en SPY.
| En el dibujo | Qué hace | Dónde |
|---|---|---|
| montón de días | histograma de rendimientos diarios en log, en desviaciones (z) | montecarlo.py:132 |
| campana de cristal | lo que esperaría una normal con la misma σ | montecarlo.py:135 |
| bloques rojos | días de más de ±4σ: reales frente a esperados (2·Φ(−4)·n) | montecarlo.py:139 |
| la gráfica en la web | barras reales frente a curva normal, en escala log | docs/demo/index.html:251 |
Dato real: SPY 2006-2026: 38 días por encima de ±4σ, cuando la campana esperaría 0,32. Curtosis en exceso 14,1. Peor día de Apple: −17,9%.
El bichito gira un bombo de lotería cuyas bolas salen de una campana N(μ, σ). Cada bola es un día; 63 bolas seguidas son un futuro de 3 meses.
Toma los últimos 5 años (1.260 días). Mide la subida media diaria y lo que se mueve: μ = 20,8% y σ = 27,9% al año en Apple. Luego sortea cada día de una campana con esos dos números.
Es la fórmula del libro, ST = S0·exp((μ − σ²/2)T + σ√T·Z), la misma que hay debajo de Black–Scholes. Se puede comprobar con lápiz, y por eso es la primera prueba de que el código está bien.
| En el dibujo | Qué hace | Dónde |
|---|---|---|
| bombo | sortea 10.000 × 63 rendimientos de N(media, desviación) | montecarlo.py:48 |
| camino de bolas | suma acumulada de logs → precio (exp de la suma) | montecarlo.py:169 |
| lápiz y papel | test: simulado frente a fórmula cerrada lognormal | test_montecarlo.py:38 |
| la fórmula en la web | KaTeX con los números reales de la acción | docs/demo/index.html:273 |
Dato real: Apple 3m, P(ganar): 62,05% simulado frente a 61,89% exacto; el azar de 10.000 caminos permite ±0,95 puntos. VaR 95%: 17,0% frente a 17,1%.
El bichito corta trozos de 21 días de la película real de los últimos años y los empalma en otro orden: un futuro nuevo hecho con días que sí pasaron.
Este motor no supone ninguna campana: sortea meses enteros reales (bloques de 21 días) de los últimos 5 años y los pega uno detrás de otro.
¿Por qué bloques y no días sueltos? Los días malos vienen en racimo: tras un crash suele venir otro día malo (agrupamiento de volatilidad). Con un mes entero, el racimo sobrevive.
| En el dibujo | Qué hace | Dónde |
|---|---|---|
| tira de película | rendimientos diarios en log de la ventana de calibración | montecarlo.py:212 |
| tijeras | inicio aleatorio de cada bloque (np.random.integers) | montecarlo.py:54 |
| empalme | índices bloque a bloque, recortados a los días del horizonte | montecarlo.py:55 |
| test | comprueba que solo usa días reales y la longitud pedida | test_montecarlo.py:19 |
Dato real: Apple 3m, Expected Shortfall 95%: 20,1% con bootstrap frente a 21,8% con GBM. Probabilidad de ganar con 5 semillas distintas: entre 63,1% y 64,3%.
En cada fecha del pasado, el simulador hace su abanico. El bichito clava una chincheta en el punto de ese abanico donde cayó el precio real, y luego se cuenta cuántas chinchetas quedaron dentro de la franja prometida.
Es como juzgar al hombre del tiempo: no por un día, sino por si llueve el 70% de las veces que dice «70%».
En cada fecha (una por mes desde 2011) el simulador solo ve los 5 años anteriores. Se mira qué fracción de sus caminos quedó por debajo del precio real: es el PIT (probability integral transform). Si el modelo es honesto, los PIT salen repartidos por igual entre 0 y 1.
| En el dibujo | Qué hace | Dónde |
|---|---|---|
| regla 0-100% | el abanico de esa fecha, ordenado de peor a mejor | montecarlo.py:188 |
| chincheta (GBM) | PIT exacto con la normal, sin simular | montecarlo.py:191 |
| chincheta (bootstrap) | PIT con 2.000 caminos por fecha | montecarlo.py:192 |
| hoja de recuento | cobertura: % de PIT dentro de cada franja | montecarlo.py:199 |
| calendarios | fechas que se pisan → pruebas independientes = fechas·21/días | montecarlo.py:194 |
Dato real: Apple 3m: 177 fechas, equivalentes a 59 pruebas independientes. Franja del 90%: GBM 94,4%, bootstrap 93,2%, y las dos caben en el margen de suerte de ±7,6 puntos.
El bichito lleva un sobre con «Apple» por un camino de cinco paradas, desde la caja de texto hasta los seis pasos que se pintan en pantalla.
Pulsas «Simular». La web llama al servidor, que busca la empresa por su nombre y elige la cotización que más dinero mueve. Calcula los dos motores y el examen y lo devuelve todo de una vez. Tarda unos 4 segundos.
| En el dibujo | Qué hace | Dónde |
|---|---|---|
| 1 · portátil | lee el texto y el horizonte y llama al servidor (fetch) | docs/demo/index.html:377 |
| 2 · buzón | servidor stdlib: /api/sim → analyze() | server.py:31 |
| 3 · lupa | Yahoo → candidatos → el más negociado con datos limpios | montecarlo.py:117 |
| 4 · engranajes | simulate() con los dos motores + exam() | montecarlo.py:165 |
| 5 · cartel | pinta el titular, el cono, las cifras y el examen | docs/demo/index.html:370 |
Dato real: «Inditex» no devuelve ITX.MC en Yahoo: se resuelve a IXD1.DE (Xetra, mismos euros). El ticker exacto ITX.MC se respeta.
El bichito levanta los dos boletines tras examinar a los motores en 16 acciones con los tests que usa un banco: la campana suspende la forma en la mitad y el bootstrap aprueba en todas.
Para no elegir 4 acciones que salgan bien, el examen se repite en 16: EE. UU., España, Reino Unido y ETFs, a 1 mes. Hay dos pruebas formales:
Kupiec (la de Basilea) cuenta las roturas del VaR 95%. Kolmogorov–Smirnov mira si los PIT están repartidos por igual, o sea, si acierta la forma entera. El GBM casi siempre pasa Kupiec, pero suspende KS en 8 de 16: mirando solo el VaR, no ves el fallo.
| En el dibujo | Qué hace | Dónde |
|---|---|---|
| boletín GBM | franja del 50% → 61,9%; roturas del VaR 95% → 3,5% | validar.py:21 |
| boletín bootstrap | franja del 50% → 51,6%; roturas → 4,9% | validar.py:21 |
| Kupiec + KS | chi² con 1 g.l. sobre roturas; KS frente a la uniforme | validar.py:28 |
| la diagonal en la web | curva prometido-vs-real + margen binomial | docs/demo/index.html:338 |
Dato real: SPY a 1 mes, 179 pruebas: el GBM dice 50% y acierta el 73,2%; el bootstrap, el 53,6%. El margen de suerte es de ±7,3 puntos: el fallo del GBM no es casualidad.
El proyecto se construyó en un día. El bichito sube la escalera y se para delante del peldaño roto: dos veces algo parecía bien y no lo estaba.
Salió del reel de @quant.with.isamu (nº 2 de 15). Tus otros vídeos avisaban de que es un proyecto cliché, así que la idea fue añadir el examen.
Hubo dos tropiezos de verdad. «Inditex» se resolvía a la bolsa de Hannover, con precios rotos (curtosis de 1.301): se arregló con un filtro de calidad. Y algunas frases de la web afirmaban más de lo que da la muestra: se arreglaron con el margen binomial.
| En el dibujo | Qué hace | Dónde |
|---|---|---|
| peldaño 4 | examen sobre datos que sí son campana → ≈90% | test_montecarlo.py:23 |
| peldaño 5 (roto) | Hannover: días congelados o saltos imposibles → se descarta la bolsa | montecarlo.py:78 |
| peldaño 7 (roto) | «exagera el riesgo» con 59 pruebas → margen ±7,6 | docs/demo/index.html:338 |
| peldaño 8 | validación en 16 acciones con Kupiec y KS | validar.py:14 |
Dato real: Inditex en IXD1.HA (Hannover): curtosis 1.301,7. En ITX.MC (Madrid): 15,5. Misma empresa, datos de otra bolsa.
Cuando algo falla, el bichito abre el cuadro y mira con la lupa qué fusible saltó. Cada fusible es un síntoma con su sitio en el código.
Cada fallo probable viene con la pista de dónde mirar. El más traicionero es «cifras raras»: no da ningún error, solo números absurdos, como una curtosis de 1.301 o un VaR enorme. Casi siempre es una bolsa secundaria con datos rotos.
| En el dibujo | Qué hace | Dónde |
|---|---|---|
| no la encuentra | Yahoo no devuelve la bolsa principal → usa el ticker exacto (ITX.MC) | montecarlo.py:99 |
| tarda mucho | el examen bootstrap: 2.000 caminos × ~180 fechas → bajar N_EXAM | montecarlo.py:32 |
| puerto ocupado | 8000 lo usa otra app → PORT=8005 | server.py:47 |
| Yahoo cambia | yfinance cambia columnas o límites → todo pasa por load() | montecarlo.py:66 |
| cifras raras | filtro: >10% de días sin cambio o salto >70% → se descarta | montecarlo.py:78 |
Dato real: Un aviso que no es error: los 404 de yfinance al probar «APPLE» como ticker. Se silencian en montecarlo.py:59 porque son parte de la búsqueda.
Un modelo no es bueno porque acierte una vez:
es bueno si sus «90%» se cumplen el 90% de las veces.