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.

Fig. — Decimales exactos del total al sumar n veces 0,1 en coma flotante (float de 64 bits)
01/06
Módulo
0.3 · Fundamentos del lenguaje
Duración
45 min
Lecciones del nivel 0
  1. 0.1 · Bienvenida y expectativas

    1. 0.1.1 Trading sistemático, algorítmico y cuantitativo frente al discrecional
    2. 0.1.2 Expectativas realistas: costes, tiempo y riesgo de pérdida
    3. 0.1.3 Estructura del curso, tests de entrada, repositorio personal y aprendizaje con IA
  2. 0.2 · Entorno de trabajo moderno

    1. 0.2.1 Instalación de Python con uv: versiones y ciclo de soporte
    2. 0.2.2 Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles
    3. 0.2.3 Editores (VS Code, Cursor, PyCharm) y notebooks (Jupyter, JupyterLab, marimo)
    4. 0.2.4 Terminal básica, rutas, variables de entorno y ficheros .env
    5. 0.2.5 Git y GitHub: commits, ramas, pull requests y .gitignore
    6. 0.2.6 Introducción a los asistentes de código con IA: prompts, revisión y tests
  3. 0.3 · Fundamentos del lenguaje

    1. 0.3.1 Variables, tipos y operadores; coma flotante y decimal para precios y ticks
    2. 0.3.2 Cadenas, f-strings y formato de números y fechas
    3. 0.3.3 Listas, tuplas, diccionarios, conjuntos y comprensiones
    4. 0.3.4 Control de flujo: if, elif, else, for, while y match-case
    5. 0.3.5 Funciones: argumentos, *args y **kwargs, lambdas y ámbito
    6. 0.3.6 Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15
  4. 0.4 · Python intermedio

    1. 0.4.1 Programación orientada a objetos: clases, herencia, composición y dataclasses
    2. 0.4.2 Tipado: type hints, mypy o ty y Pydantic para configuraciones
    3. 0.4.3 Iteradores, generadores, decoradores y context managers
    4. 0.4.4 Ficheros (CSV, JSON, YAML, TOML) y fechas con zonas horarias
    5. 0.4.5 Logging profesional y testing con pytest
    6. 0.4.6 Concurrencia: threading, multiprocessing, asyncio y free-threading
  5. 0.5 · Computación numérica y datos

    1. 0.5.1 NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios
    2. 0.5.2 pandas 3.0: Series, DataFrame, índices temporales, Copy-on-Write, tipo str y pd.col()
    3. 0.5.3 Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles
    4. 0.5.4 Polars: API lazy frente a eager, expresiones, rendimiento y cuándo elegirlo frente a pandas
    5. 0.5.5 DuckDB y SQL esencial: SELECT, JOIN, GROUP BY y funciones de ventana sobre Parquet
  6. 0.6 · Visualización

    1. 0.6.1 Matplotlib orientado a objetos, Seaborn y Plotly: gráficos de precios, drawdown y velas
    2. 0.6.2 Dashboards rápidos con Streamlit: un panel de seguimiento en un solo script
  7. 0.7 · Matemáticas y estadística mínimas

    1. 0.7.1 Rentabilidades simples y logarítmicas, interés compuesto y estadística descriptiva
    2. 0.7.2 Probabilidad y distribuciones, correlación frente a causalidad y regresión simple
    3. 0.7.3 Álgebra lineal intuitiva: vectores y matrices como preparación para carteras

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

IN [01]python
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))
OUT [01]Cuatro variables de una orden
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:

TipoQué guardaEjemplo de tradingLiteral en Python
intEnteros, sin límite de tamañoAcciones de una orden, contratos, volumen100, -3, 2_322_609
floatNúmeros con decimales (coma flotante binaria)Precios, rentabilidades, volatilidad142.22, -0.035, 1e-4
strTextoTicker, mercado, lado de la orden"SINT", "compra"
boolVerdadero o falso¿Hay posición abierta? ¿Se tocó el stop?True, False
NoneTypeLa ausencia de valorUn stop que todavía no se ha fijadoNone

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:

IN [02]python
stop = None          # todavía sin stop
stop = 138.50        # ahora es un float
print(stop, type(stop))
OUT [02]Reasignar una variable
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 **.

OperadorOperaciónEjemploResultado
+ - *Suma, resta, producto142.22 * 10014222.0
/División (siempre float)10 / 25.0
//División entera (hacia abajo)1250 // 10012
%Resto de la división entera1250 % 10050
**Potencia1.05 ** 101.628…

El valor de una posición, su resultado y su rentabilidad salen de multiplicar, restar y dividir:

IN [03]python
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)
OUT [03]Valor, P&L y rentabilidad de una posición
14222.0
681.9999999999993
0.050369276218611425

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

IN [04]python
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)
OUT [04]Lotes y contratos
Lotes completos: 12
Acciones sueltas: 50
Contratos: 4
Capital sin usar: 1000
Capital al 5 % anual en 10 años: 16288.94626777442

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

IN [05]python
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)
OUT [05]Condiciones de una regla
True False True

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

IN [06]python
from decimal import Decimal

print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
print(Decimal(0.1))
print(Decimal(142.22))
OUT [06]El error de representación
0.30000000000000004
False
0.1000000000000000055511151231257827021181583404541015625
142.219999999999998863131622783839702606201171875

Decimal(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ón if precio == objetivo que nunca se cumple es un error difícil de ver.
  • Contar ticks con int(). int no 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) da 2.67, porque 2,675 está guardado como 2,67499999…; además, round redondea los empates al par (round(0.125, 2) es 0.12).
IN [07]python
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)
OUT [07]Ticks contados con float
3.0000000000001137 3
9.999999999999432 9
2.67 0.12
3.0

Entre 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):

IN [08]python
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}")
OUT [08]Sumar 0,10 € muchas veces
        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-04

total += 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 float basta. 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 en float por 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:

IN [09]python
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"))
OUT [09]Decimal desde cadenas
True
0.1 False
682.00
10

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

IN [10]python
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))
OUT [10]Redondeo al tick según el lado
101.24
101.23
Compra límite: 101.20  Venta límite: 101.25
2.68 2.66

Sin 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):

IN [11]python
Decimal("142.22") + 0.01
OUT [11]Decimal y float no se mezclan
TypeError: 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

DatoTipoPor qué
Acciones, contratos, lotesintSon enteros: con // y %, nunca con int() sobre un float
Rentabilidades, volatilidad, señalesfloatVelocidad; el error relativo de 10⁻¹⁶ no cambia ninguna decisión
Precios de órdenes, ticksDecimal o int en ticksTienen que caer exactamente en la rejilla del mercado
Saldos, comisiones, impuestosDecimalDeben cuadrar al céntimo con el extracto del bróker
Precios en un backtest vectorialfloat (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

  1. 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
IN [12]python
capital = 47_300
garantia = 5_500
contratos = capital // garantia
print("Contratos:", contratos)
print("Sin usar:", capital % garantia)
print("Comprometido:", contratos * garantia / capital)
OUT [12]Contratos con 47.300 €
Contratos: 8
Sin usar: 3300
Comprometido: 0.9302325581395349

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

  1. 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
IN [13]python
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)
OUT [13]Rejilla de 0,05
87.40 87.45
0.00 0.00

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

  1. Cuenta los ticks de 0,01 que hay entre 99,95 y 100,05 de tres formas: con int() sobre el float, con round() sobre el float y con Decimal. ¿Cuál falla y por qué?
Solución
IN [14]python
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"))
OUT [14]Tres formas de contar ticks
9
10
10

Falla 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

  1. 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?

    • A25_000 / 6_000
    • B25_000 // 6_000
    • C25_000 % 6_000
    • Dround(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 €) y round daría 4 aquí, pero redondearía hacia arriba con 5,5 contratos y abriría uno que no se puede cubrir.

  2. ¿Qué devuelve 0.1 + 0.2 == 0.3 en Python?

    • ATrue
    • BFalse, 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 binario

    Los float se 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.

  3. Un precio en float está 10 ticks de 0,01 por encima de otro, pero int((100.05 - 99.95) / 0.01) devuelve 9. ¿Por qué?

    • APorque int redondea al entero más cercano
    • BPorque la resta en coma flotante da 9,99999999999943… e int trunca 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 int trunca los decimales

    int() corta los decimales, no redondea. Con un error de representación justo por debajo del entero, el recuento de ticks pierde uno. Con Decimal creado desde cadenas la resta es exacta.

  4. 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: int para cantidades, float para cálculos, str para tickers y bool para condiciones. type() lo muestra.
  • / siempre da un float; // y % dan la parte entera y el resto: lotes completos, contratos y acciones sueltas.
  • Los float son binarios: 0.1 + 0.2 no es 0.3. Nunca se comparan con == ni se convierten con int() sin redondear antes.
  • El error de un float es diminuto y se acumula despacio: para estadística y backtests vectoriales basta. Para precios de órdenes y contabilidad, Decimal.
  • Decimal se crea desde cadenas, no desde float, y quantize redondea al tick con el modo que pida el lado de la orden.

Recursos