Lección 0.3.1 · 45 min
Variables, tipos y operadores; coma flotante y decimal para precios y ticks
Un precio, una cantidad y un ticker son datos de tipos distintos, y Python los trata de forma distinta. La lección presenta los tipos básicos y sus operadores, y el primer error clásico del trading en código: la coma flotante.
- Módulo
- 0.3 · Fundamentos del lenguaje
- Duración
- 45 min
Lecciones del nivel 0
0.1 · Bienvenida y expectativas
0.2 · Entorno de trabajo moderno
- 0.2.1 Instalación de Python con uv: versiones y ciclo de soporte
- 0.2.2 Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles
- 0.2.3 Editores (VS Code, Cursor, PyCharm) y notebooks (Jupyter, JupyterLab, marimo)
- 0.2.4 Terminal básica, rutas, variables de entorno y ficheros .env
- 0.2.5 Git y GitHub: commits, ramas, pull requests y .gitignore
- 0.2.6 Introducción a los asistentes de código con IA: prompts, revisión y tests
0.3 · Fundamentos del lenguaje
- 0.3.1 Variables, tipos y operadores; coma flotante y decimal para precios y ticks
- 0.3.2 Cadenas, f-strings y formato de números y fechas
- 0.3.3 Listas, tuplas, diccionarios, conjuntos y comprensiones
- 0.3.4 Control de flujo: if, elif, else, for, while y match-case
- 0.3.5 Funciones: argumentos, *args y **kwargs, lambdas y ámbito
- 0.3.6 Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15
0.4 · Python intermedio
- 0.4.1 Programación orientada a objetos: clases, herencia, composición y dataclasses
- 0.4.2 Tipado: type hints, mypy o ty y Pydantic para configuraciones
- 0.4.3 Iteradores, generadores, decoradores y context managers
- 0.4.4 Ficheros (CSV, JSON, YAML, TOML) y fechas con zonas horarias
- 0.4.5 Logging profesional y testing con pytest
- 0.4.6 Concurrencia: threading, multiprocessing, asyncio y free-threading
0.5 · Computación numérica y datos
- 0.5.1 NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios
- 0.5.2 pandas 3.0: Series, DataFrame, índices temporales, Copy-on-Write, tipo str y pd.col()
- 0.5.3 Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles
- 0.5.4 Polars: API lazy frente a eager, expresiones, rendimiento y cuándo elegirlo frente a pandas
- 0.5.5 DuckDB y SQL esencial: SELECT, JOIN, GROUP BY y funciones de ventana sobre Parquet
0.6 · Visualización
0.7 · Matemáticas y estadística mínimas
Variables: nombres para datos del mercado
Un programa trabaja con datos: el precio de un activo, la cantidad de una orden, el símbolo que se opera. Una
variable es un nombre que apunta a uno de esos datos. Se crea con el signo =, que en Python no significa
«igual», sino «asigna a este nombre el valor de la derecha»:
ticker = "SINT"
precio = 142.22
cantidad = 100
es_compra = True
print(ticker, precio, cantidad, es_compra)
print(type(ticker), type(precio), type(cantidad), type(es_compra))SINT 142.22 100 True
<class 'str'> <class 'float'> <class 'int'> <class 'bool'>Cada valor tiene un tipo, y type() lo dice. Los cuatro de la celda son los tipos básicos que aparecen en
cualquier programa de trading:
| Tipo | Qué guarda | Ejemplo de trading | Literal en Python |
|---|---|---|---|
int | Enteros, sin límite de tamaño | Acciones de una orden, contratos, volumen | 100, -3, 2_322_609 |
float | Números con decimales (coma flotante binaria) | Precios, rentabilidades, volatilidad | 142.22, -0.035, 1e-4 |
str | Texto | Ticker, mercado, lado de la orden | "SINT", "compra" |
bool | Verdadero o falso | ¿Hay posición abierta? ¿Se tocó el stop? | True, False |
NoneType | La ausencia de valor | Un stop que todavía no se ha fijado | None |
Tres reglas sobre los nombres. Se escriben en minúsculas con guiones bajos (precio_entrada, no PrecioEntrada):
es la convención de Python y la que sigue todo el curso. No pueden empezar por un número ni coincidir con palabras
reservadas (if, for, class…). Y conviene que digan lo que guardan: p o x2 obligan a leer el resto del código
para saber qué son; precio_cierre no.
En el código el separador decimal es el punto (142.22), no la coma. El guion bajo dentro de un número solo sirve para
leerlo mejor: 2_322_609 es el mismo int que 2322609.
Una variable se puede reasignar, y desde ese momento apunta al valor nuevo. El tipo lo lleva el valor, no el nombre:
stop = None # todavía sin stop
stop = 138.50 # ahora es un float
print(stop, type(stop))138.5 <class 'float'>Python muestra 138.5, no 138.50: un float no guarda cuántos decimales se escribieron, solo el número. Cómo
mostrar siempre dos decimales se ve en la lección 0.3.2.
Operadores aritméticos
Los operadores combinan valores. Los aritméticos son los de una calculadora, más tres que en trading se usan a diario:
la división entera //, el resto % y la potencia **.
| Operador | Operación | Ejemplo | Resultado |
|---|---|---|---|
+ - * | Suma, resta, producto | 142.22 * 100 | 14222.0 |
/ | División (siempre float) | 10 / 2 | 5.0 |
// | División entera (hacia abajo) | 1250 // 100 | 12 |
% | Resto de la división entera | 1250 % 100 | 50 |
** | Potencia | 1.05 ** 10 | 1.628… |
El valor de una posición, su resultado y su rentabilidad salen de multiplicar, restar y dividir:
precio_entrada = 135.40
precio_actual = 142.22
cantidad = 100
valor = precio_actual * cantidad
pnl = (precio_actual - precio_entrada) * cantidad
rentabilidad = precio_actual / precio_entrada - 1
print(valor)
print(pnl)
print(rentabilidad)14222.0
681.9999999999993
0.050369276218611425La salida ya avisa del tema central de la lección: el P&L, que debería ser exactamente 682 €, aparece con una cola de decimales. Antes de explicarlo, los otros dos operadores.
La división entera y el resto resuelven un problema que aparece en cuanto se calcula el tamaño de una orden: muchas
órdenes solo admiten múltiplos de un lote, y los futuros se operan en contratos enteros. // da cuántos caben y %
lo que sobra:
acciones = 1_250
lote = 100
print("Lotes completos:", acciones // lote)
print("Acciones sueltas:", acciones % lote)
capital = 25_000
garantia_contrato = 6_000
print("Contratos:", capital // garantia_contrato)
print("Capital sin usar:", capital % garantia_contrato)
print("Capital al 5 % anual en 10 años:", 10_000 * 1.05 ** 10)Lotes completos: 12
Acciones sueltas: 50
Contratos: 4
Capital sin usar: 1000
Capital al 5 % anual en 10 años: 16288.94626777442Dos detalles de esta celda. print admite varios valores separados por comas y los escribe con un espacio entre
ellos. Y el orden de las operaciones es el de las matemáticas: primero **, después *, /, // y %, y al final
+ y -. Por eso 10_000 * 1.05 ** 10 eleva antes de multiplicar. Ante la duda, paréntesis.
Hay una diferencia entre / y // que conviene fijar desde el principio: / devuelve siempre un float, aunque la
división sea exacta (10 / 2 es 5.0). Si el resultado va a contar acciones o contratos, tiene que salir de // o
de un redondeo explícito, nunca de pasar un float a int sin más (se ve abajo por qué).
Comparaciones y lógica
Los operadores de comparación (==, !=, <, <=, >, >=) devuelven un bool, y and, or y not los
combinan. Son la materia prima de cualquier regla de trading:
cierre = 142.22
media_200 = 131.80
stop = 138.50
por_encima = cierre > media_200
stop_tocado = cierre <= stop
mantener = por_encima and not stop_tocado
print(por_encima, stop_tocado, mantener)True False TrueOjo con la diferencia entre = (asignar) y == (comparar). Qué hacer con estas condiciones —comprar, vender, no
hacer nada— es materia de la lección 0.3.4.
Coma flotante: por qué 0,1 + 0,2 no es 0,3
Un float se guarda en binario, con 53 bits de precisión (la norma IEEE 754, la misma en cualquier ordenador).
Igual que 1/3 no tiene una expresión decimal finita (0,3333…), la mayoría de las fracciones decimales no tienen una
expresión binaria finita: 0,1 es en base 2 una fracción periódica. El ordenador guarda el número binario más cercano,
y la diferencia aparece en cuanto se opera:
from decimal import Decimal
print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
print(Decimal(0.1))
print(Decimal(142.22))0.30000000000000004
False
0.1000000000000000055511151231257827021181583404541015625
142.219999999999998863131622783839702606201171875Decimal(0.1) muestra el valor exacto que hay en memoria cuando se escribe 0.1: casi, pero no exactamente, una
décima. Lo mismo pasa con casi cualquier precio con decimales. print lo disimula porque muestra el número más corto
que vuelve a dar el mismo float, pero el valor guardado es el de la última línea.
En trading, este error de representación produce tres fallos típicos:
- Comparar con
==. Un precio calculado (entrada + 3 * tick) puede no ser igual al precio que llega del mercado aunque los dos se lean igual. Una condiciónif precio == objetivoque nunca se cumple es un error difícil de ver. - Contar ticks con
int().intno redondea: corta los decimales. Si la división da 9,99999999999943…, el resultado es 9. - Redondear al céntimo con
round().round(2.675, 2)da2.67, porque 2,675 está guardado como 2,67499999…; además,roundredondea los empates al par (round(0.125, 2)es0.12).
tick = 0.01
print((101.03 - 101.00) / tick, int((101.03 - 101.00) / tick))
print((100.05 - 99.95) / tick, int((100.05 - 99.95) / tick))
print(round(2.675, 2), round(0.125, 2))
tick_futuro = 0.25
print((4500.75 - 4500.00) / tick_futuro)3.0000000000001137 3
9.999999999999432 9
2.67 0.12
3.0Entre 99,95 y 100,05 hay diez ticks de 0,01; int cuenta nueve. La última línea, en cambio, es exacta: 0,25 es 1/4,
una fracción binaria finita, así que los precios en cuartos se representan sin error. Los ticks de 0,01, 0,05 o 0,1,
no.
Para comparar precios en coma flotante, la regla es comparar con una tolerancia (math.isclose(a, b) o
abs(a - b) < tick / 2) y, para contar ticks, redondear antes de convertir: round((b - a) / tick).
Cuánto se acumula el error
El error de cada operación es del orden de 10⁻¹⁶ veces el valor. Lo que importa es si crece al sumar muchas
operaciones, como en un libro de operaciones o en el P&L de un backtest largo. La celda suma 0,10 € de beneficio
una y otra vez y mide la diferencia con el resultado exacto (el bucle for repite la suma; se explica en la lección
0.3.4):
from decimal import Decimal
for n in [10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000]:
total = 0.0
for _ in range(n):
total += 0.1
exacto = Decimal(n) / 10
error = abs(Decimal(total) - exacto)
print(f"{n:>10,} {total!r:<20} error {float(error):.1e}") 10 0.9999999999999999 error 1.1e-16
100 9.99999999999998 error 2.0e-14
1,000 99.9999999999986 error 1.4e-12
10,000 1000.0000000001588 error 1.6e-10
100,000 10000.000000018848 error 1.9e-08
1,000,000 100000.00000133288 error 1.3e-06
10,000,000 999999.9998389754 error 1.6e-04total += 0.1 es una forma corta de escribir total = total + 0.1. La figura de la cabecera traduce esos errores a
decimales exactos: cuántos decimales del total coinciden con la cuenta exacta. Tras diez sumas, quince; tras diez
millones, tres. Dos conclusiones, las dos importantes:
- Para estadística, rentabilidades y backtests, el
floatbasta. Un error en el cuarto decimal tras diez millones de sumas no cambia ninguna decisión, y NumPy y pandas (módulo 0.5) trabajan enfloatpor velocidad. - Para precios de órdenes, saldos de cuenta y conciliación con el bróker, no basta. Ahí el problema no es el tamaño del error, sino que exista: un precio que no cae en la rejilla de ticks o un saldo que no cuadra al céntimo es un error, aunque sea pequeño.
Decimal: precios exactos y redondeo a tick
El módulo decimal de la biblioteca estándar representa números en base 10, como en la contabilidad. Un Decimal
creado desde una cadena guarda exactamente lo que se escribe:
from decimal import Decimal
print(Decimal("0.1") + Decimal("0.2") == Decimal("0.3"))
print(Decimal("0.1"), Decimal(0.1) == Decimal("0.1"))
entrada = Decimal("135.40")
salida = Decimal("142.22")
pnl = (salida - entrada) * 100
print(pnl)
print((Decimal("100.05") - Decimal("99.95")) / Decimal("0.01"))True
0.1 False
682.00
10El P&L sale exacto, con sus dos decimales, y el recuento de ticks es 10. La segunda línea muestra la trampa:
Decimal(0.1), sin comillas, copia el float con su error. Los precios se pasan a Decimal como texto: tal como
llegan del bróker o del fichero, o con Decimal(str(precio)) si ya son float.
Para llevar un precio a la rejilla de ticks hay dos casos. Si el tick es una potencia de diez (0,01, 0,001), basta con
quantize, que redondea a los decimales del valor que recibe. Si no lo es (0,05, 0,25), se divide por el tick, se
redondea a entero y se vuelve a multiplicar. En los dos casos, el modo de redondeo depende del lado de la orden:
una compra límite se redondea hacia abajo, para no pagar más que el precio calculado, y una venta límite hacia arriba.
from decimal import ROUND_CEILING, ROUND_FLOOR, ROUND_HALF_EVEN
precio = Decimal("101.237")
print(precio.quantize(Decimal("0.01")))
print(precio.quantize(Decimal("0.01"), rounding=ROUND_FLOOR))
tick = Decimal("0.05")
unidades = precio / tick
compra = unidades.quantize(Decimal("1"), rounding=ROUND_FLOOR) * tick
venta = unidades.quantize(Decimal("1"), rounding=ROUND_CEILING) * tick
print("Compra límite:", compra, " Venta límite:", venta)
print(Decimal("2.675").quantize(Decimal("0.01")),
Decimal("2.665").quantize(Decimal("0.01"),
rounding=ROUND_HALF_EVEN))101.24
101.23
Compra límite: 101.20 Venta límite: 101.25
2.68 2.66Sin rounding, quantize usa el modo por defecto, ROUND_HALF_EVEN: el más cercano y, en caso de empate, el par
(el «redondeo del banquero», que no sesga los totales hacia arriba). Con Decimal, 2,675 sí es un empate y se
redondea a 2,68; 2,665 se redondea a 2,66, el par. ROUND_HALF_UP es el redondeo escolar; ROUND_FLOOR y
ROUND_CEILING, hacia abajo y hacia arriba.
La exactitud tiene un precio. Decimal es más lento que float y no se mezcla con él: sumar un Decimal y un
float es un error, para que nadie meta por descuido el error binario en una cuenta exacta. Python lo señala con una
excepción (las excepciones se ven en la lección 0.3.6):
Decimal("142.22") + 0.01TypeError: unsupported operand type(s) for +: 'decimal.Decimal' and 'float'Y la precisión de Decimal también es finita: 28 cifras significativas por defecto. Las divisiones que no son exactas
se redondean ahí, así que Decimal no arregla 1/3; arregla los decimales que se escriben en base 10, que son los de
los precios.
Qué tipo usar en cada caso
| Dato | Tipo | Por qué |
|---|---|---|
| Acciones, contratos, lotes | int | Son enteros: con // y %, nunca con int() sobre un float |
| Rentabilidades, volatilidad, señales | float | Velocidad; el error relativo de 10⁻¹⁶ no cambia ninguna decisión |
| Precios de órdenes, ticks | Decimal o int en ticks | Tienen que caer exactamente en la rejilla del mercado |
| Saldos, comisiones, impuestos | Decimal | Deben cuadrar al céntimo con el extracto del bróker |
| Precios en un backtest vectorial | float (NumPy/pandas) | Se comparan con tolerancia y se redondean al tick al generar la orden |
La segunda opción para los precios, guardar el número de ticks como int (10.123 ticks de 0,01 en lugar de 101,23),
es la que usan muchos motores de bolsa: los enteros son exactos y rápidos.
Ejercicios
- Una cuenta tiene 47.300 € y cada contrato de un futuro exige 5.500 € de garantía. Calcula cuántos contratos se pueden abrir, cuánto capital queda sin usar y qué porcentaje de la cuenta queda comprometido como garantía.
Solución
capital = 47_300
garantia = 5_500
contratos = capital // garantia
print("Contratos:", contratos)
print("Sin usar:", capital % garantia)
print("Comprometido:", contratos * garantia / capital)Contratos: 8
Sin usar: 3300
Comprometido: 0.9302325581395349Ocho contratos y 3.300 € sin usar: algo más del 93 % de la cuenta queda como garantía. Que se pueda no significa que se deba: el tamaño de posición se decide por riesgo (nivel 4), no por la garantía disponible.
- Un precio de compra calculado por una estrategia es 87,4361 y el mercado cotiza en ticks de 0,05. Con
Decimal, calcula el precio límite de compra (hacia abajo) y el de venta (hacia arriba), y comprueba que los dos caen en la rejilla: el resto de dividirlos por el tick tiene que ser cero.
Solución
from decimal import Decimal, ROUND_CEILING, ROUND_FLOOR
precio = Decimal("87.4361")
tick = Decimal("0.05")
uno = Decimal("1")
compra = (precio / tick).quantize(uno, rounding=ROUND_FLOOR) * tick
venta = (precio / tick).quantize(uno, rounding=ROUND_CEILING) * tick
print(compra, venta)
print(compra % tick, venta % tick)87.40 87.45
0.00 0.00El operador % también funciona con Decimal. Con float, el mismo resto daría un número diminuto en lugar de
cero y la comprobación fallaría.
- Cuenta los ticks de 0,01 que hay entre 99,95 y 100,05 de tres formas: con
int()sobre elfloat, conround()sobre elfloaty conDecimal. ¿Cuál falla y por qué?
Solución
from decimal import Decimal
print(int((100.05 - 99.95) / 0.01))
print(round((100.05 - 99.95) / 0.01))
print((Decimal("100.05") - Decimal("99.95")) / Decimal("0.01"))9
10
10Falla int(), que trunca 9,99999999999943… a 9. round() lo arregla porque el error es mucho menor que medio tick, y
Decimal da el resultado exacto desde el principio.
Respuesta plegada en cada pregunta
Autoevaluación
Una cuenta tiene 25.000 € y cada contrato de un futuro exige 6.000 € de garantía. ¿Qué expresión da el número de contratos que se pueden abrir?
- A
25_000 / 6_000 - B
25_000 // 6_000 - C
25_000 % 6_000 - D
round(25_000 / 6_000)
Ver respuesta
Respuesta B.
25_000 // 6_000//es la división entera: 4 contratos./da 4,1666…,%da el resto (1.000 €) yrounddaría 4 aquí, pero redondearía hacia arriba con 5,5 contratos y abriría uno que no se puede cubrir.- A
¿Qué devuelve
0.1 + 0.2 == 0.3en Python?- A
True - B
False, porque 0,1 y 0,2 no tienen representación exacta en binario - CUn error de tipo
- DDepende del sistema operativo
Ver respuesta
Respuesta B.
False, porque 0,1 y 0,2 no tienen representación exacta en binarioLos
floatse guardan en binario y 0,1 es una fracción periódica en base 2. La suma da 0,30000000000000004. El resultado es el mismo en cualquier máquina: lo fija la norma IEEE 754.- A
Un precio en
floatestá 10 ticks de 0,01 por encima de otro, peroint((100.05 - 99.95) / 0.01)devuelve 9. ¿Por qué?- APorque
intredondea al entero más cercano - BPorque la resta en coma flotante da 9,99999999999943… e
inttrunca los decimales - CPorque 0,01 no es un tick válido
- DPor un error del intérprete
Ver respuesta
Respuesta B. Porque la resta en coma flotante da 9,99999999999943… e
inttrunca los decimalesint()corta los decimales, no redondea. Con un error de representación justo por debajo del entero, el recuento de ticks pierde uno. ConDecimalcreado desde cadenas la resta es exacta.- APorque
Hay que enviar una orden límite de compra y el precio calculado, 101,237, no está en la rejilla de ticks de 0,05. ¿Qué redondeo es el prudente?
- AHacia arriba, a 101,25, para que se ejecute antes
- BHacia abajo, a 101,20, para no pagar más que el precio calculado
- CAl más cercano siempre, sin mirar el lado
- DNo redondear: el bróker lo acepta
Ver respuesta
Respuesta B. Hacia abajo, a 101,20, para no pagar más que el precio calculado
Una compra límite redondeada hacia abajo nunca paga más de lo calculado; una venta, hacia arriba. Si el precio no cae en la rejilla, el bróker rechaza la orden.
Ideas clave
- Cada valor tiene un tipo:
intpara cantidades,floatpara cálculos,strpara tickers yboolpara condiciones.type()lo muestra. /siempre da unfloat;//y%dan la parte entera y el resto: lotes completos, contratos y acciones sueltas.- Los
floatson binarios:0.1 + 0.2no es0.3. Nunca se comparan con==ni se convierten conint()sin redondear antes. - El error de un
floates diminuto y se acumula despacio: para estadística y backtests vectoriales basta. Para precios de órdenes y contabilidad,Decimal. Decimalse crea desde cadenas, no desdefloat, yquantizeredondea al tick con el modo que pida el lado de la orden.