Por qué probar y cómo es una prueba
En trading, un error no es una excepción en pantalla; es un número equivocado que parece plausible: un signo cambiado en un P&L, una comisión aplicada dos veces, una señal que usa en silencio el precio de mañana. Las pruebas son pequeños programas que comprueban tu código contra respuestas conocidas, de modo que ese error se detecta en tu máquina y no en el mercado. pytest encuentra las funciones llamadas test_*, ejecuta cada una e informa de las que fallan un assert. Una prueba se lee sin framework: prepara una entrada, llama a la función y haz assert de lo que esperas.
# A test is a function with an assert; pytest runs every test_* function it finds
def net_pnl(entry, exit_price, quantity, fee):
return (exit_price - entry) * quantity - fee
def test_net_pnl_long_winner():
assert net_pnl(100.0, 102.0, 50, 1.0) == 99.0
def test_net_pnl_short_loser():
assert net_pnl(100.0, 102.0, -50, 1.0) == -101.0
for test in (test_net_pnl_long_winner, test_net_pnl_short_loser):
test() # pytest does this for you and reports failures
print(test.__name__, "passed")Flotantes, errores y muchos casos
Los flotantes son aproximados, así que 0.1 + 0.2 == 0.3 es falso. Compara números con tolerancia: pytest.approx(expected) acepta una pequeña diferencia relativa (por defecto una parte entre un millón, más un diminuto suelo absoluto de 1e-12) y puedes añadir una tolerancia absoluta para valores cercanos a cero, donde una relativa no sirve. Para comprobar que se rechaza una entrada mala, pytest.raises(ValueError) pasa solo si ocurre esa excepción, y falla si no ocurre ninguna (u otra). Para ejecutar una prueba con muchos casos, @pytest.mark.parametrize aporta una tabla de entradas y valores esperados, informa de cada fila por separado y convierte añadir un caso en un cambio de una línea.
# Floats need a tolerance: approx accepts a tiny relative difference, == does not
import pytest
print(0.1 + 0.2 == 0.3) # False
print(0.1 + 0.2 == pytest.approx(0.3)) # True
print(1e-10 == pytest.approx(0.0)) # False near zero a relative tolerance cannot help
print(1e-10 == pytest.approx(0.0, abs=1e-9)) # True a larger absolute tolerance does# raises: the test passes only if the expected exception happens
import pytest
def leverage(gross, equity):
if equity <= 0:
raise ValueError("equity must be positive")
return gross / equity
with pytest.raises(ValueError) as info:
leverage(1000.0, 0.0)
print("raised:", info.value) # raised: equity must be positive
try:
with pytest.raises(ValueError):
leverage(1000.0, 500.0) # no exception: pytest.raises itself fails
except BaseException as outcome:
print(type(outcome).__name__) # Failed# parametrize in miniature: one function, a table of cases, each row judged on its own
def fee(notional, rate=0.001, minimum=1.0):
return max(notional * rate, minimum)
cases = [
((100_000.0,), 100.0),
((500.0,), 1.0), # the minimum fee applies
((2_000.0, 0.002), 4.0),
((0.0,), 1.0),
]
for index, (args, expected) in enumerate(cases):
got = fee(*args)
print(index, "ok" if abs(got - expected) <= 1e-9 * abs(expected) else f"FAIL got {got}")Fixtures, semillas y qué probar
Una fixture es una preparación compartida que las pruebas piden por nombre: una serie de precios pequeña, un generador aleatorio con semilla, un archivo temporal. Con datos aleatorios, fija la semilla (o imprímela) para poder reproducir un fallo: una prueba que falla una vez de cada cien y no se puede repetir es peor que ninguna. Prueba el comportamiento que importa y no los detalles internos: casos conocidos a mano, casos límite (entrada vacía, cero, un elemento), invariantes que siempre deben cumplirse (una probabilidad está entre 0 y 1, comprar y vender al mismo precio pierde exactamente las comisiones) y una prueba de regresión por cada error que corrijas. Cuando puedes enunciar una regla válida para toda entrada, buscar un contraejemplo entre muchas entradas aleatorias es una prueba barata y potente.
# A seed makes a random test replayable; a property is searched for a counterexample
import random
def find_counterexample(prop, inputs):
for value in inputs:
if not prop(value):
return value
return None
rng = random.Random(42) # fixed seed: the same inputs on every run
returns = [round(rng.uniform(-0.05, 0.05), 4) for _ in range(1000)]
def compound_is_positive_growth(r):
return (1 + r) > 0 # a daily return above -100% keeps the value positive
print(find_counterexample(compound_is_positive_growth, returns)) # None the rule holds for all 1000
print(find_counterexample(lambda r: r > -0.049, returns)) # a return that breaks a wrong rule- Una prueba prepara la entrada, llama a la función y hace
assertdel resultado; pytest ejecuta cada funcióntest_*e informa de los fallos. - Compara flotantes con
pytest.approx(añade tolerancia absoluta cerca de cero), espera errores conpytest.raisesy usaparametrizepara tablas de casos. - Fija las semillas, prueba casos límite, invariantes y errores corregidos, busca contraejemplos y haz fallar cada prueba una vez antes de fiarte de ella.
A continuación: perfilado de código Python, medir adónde va el tiempo antes de intentar acelerarlo.
Lab: qué se califica
- Comparar flotantes con tolerancia en
is_close25 pts - Ejecutar una tabla de casos con
run_cases25 pts - Esperar un error con
raises25 pts - Cazar un fallo con
find_counterexample25 pts