5. Funciones, clases y manejo de errores
Nivel 2: Python desde Cero para Finanzas
Una función agrupa código que quieres reutilizar bajo un nombre, para no repetir la misma lógica en veinte sitios distintos del programa. Se define con la palabra clave def, seguida del nombre, los argumentos entre paréntesis, y dos puntos.
def calcular_retorno(precio_inicial, precio_final):
"""
Devuelve el retorno porcentual entre dos precios.
"""
return (precio_final - precio_inicial) / precio_inicial
calcular_retorno(180, 183.6) # 0.02, un retorno del 2%El texto entre triples comillas justo después de la definición es el docstring: una descripción de qué hace la función, que además puedes consultar en cualquier momento con help(calcular_retorno). No es obligatorio, pero en cualquier código que vaya a mantener alguien más — incluido tú mismo dentro de seis meses — es casi imprescindible.
Las funciones pueden tener argumentos con un valor por defecto, que se usa automáticamente si no lo indicas al llamarla:
def calcular_posicion(capital, riesgo_por_operacion=0.02):
"""
Devuelve cuanto capital arriesgar en una operacion,
segun un porcentaje de riesgo (por defecto, el 2%).
"""
return capital * riesgo_por_operacion
calcular_posicion(10000) # usa el 2% por defecto: 200.0
calcular_posicion(10000, 0.05) # riesgo explicito del 5%: 500.0
calcular_posicion(capital=10000, riesgo_por_operacion=0.01) # argumentos con nombre, en cualquier ordenPara funciones muy pequeñas y de un solo uso, Python permite definirlas sin nombre con lambda — típicamente para pasarlas como argumento a otra función, por ejemplo para ordenar una lista de operaciones por su retorno:
operaciones = [("AAPL", 0.02), ("TSLA", -0.01), ("MSFT", 0.05)]
operaciones_ordenadas = sorted(operaciones, key=lambda op: op[1], reverse=True)Cuando necesitas agrupar datos y el comportamiento que actúa sobre ellos en una sola unidad, entras en programación orientada a objetos, y la herramienta es la clase. Una clase define la estructura de un objeto — sus atributos, es decir sus datos — y sus métodos, las funciones que puede ejecutar.
class Posicion:
"""
Representa una posicion abierta en un activo.
"""
def __init__(self, ticker, precio_entrada, cantidad):
self.ticker = ticker
self.precio_entrada = precio_entrada
self.cantidad = cantidad
def valor_actual(self, precio_actual):
"""
Devuelve el valor de mercado actual de la posicion.
"""
return precio_actual * self.cantidad
def resultado(self, precio_actual):
"""
Devuelve la ganancia o perdida respecto al precio de entrada.
"""
return (precio_actual - self.precio_entrada) * self.cantidad
def __str__(self):
return f"Posicion de {self.cantidad} acciones de {self.ticker} a {self.precio_entrada}"El método __init__ se ejecuta automáticamente al crear un nuevo objeto de esa clase, y es donde se inicializan sus atributos. El primer argumento de cualquier método siempre es self, una referencia al propio objeto — es lo que le permite a valor_actual y a resultado acceder al ticker, al precio_entrada y a la cantidad de esa posición concreta.
mi_posicion = Posicion("AAPL", 180.00, 50)
print(mi_posicion) # usa __str__: "Posicion de 50 acciones de AAPL a 180.0"
print(mi_posicion.resultado(183.6)) # 180.0 de gananciaCada objeto creado a partir de la clase (mi_posicion, por ejemplo) tiene su propia copia independiente de los atributos: crear una segunda posición no afecta a la primera. Esa independencia es justo lo que hace útil a la programación orientada a objetos cuando gestionas una cartera con muchas posiciones abiertas a la vez.
Por último, ninguna función que trabaje con datos reales está libre de recibir algo inesperado: un ticker que no existe, un precio negativo, una división entre cero. Python gestiona estos casos con excepciones. Cuando algo va mal, se puede "lanzar" una excepción con raise, y capturarla en otro punto del programa con try/except para que el programa no se detenga de golpe.
def calcular_retorno(precio_inicial, precio_final):
if precio_inicial <= 0:
raise ValueError("El precio inicial no puede ser cero o negativo")
return (precio_final - precio_inicial) / precio_inicial
try:
r = calcular_retorno(0, 100)
except ValueError as error:
print(f"Operacion invalida: {error}")Sin el try/except, ese raise habría detenido la ejecución completa del programa. Con él, capturas el error, lo gestionas — en este caso, imprimiendo un mensaje claro — y el resto del programa puede seguir corriendo. En un sistema de trading en vivo, este patrón es la diferencia entre que un solo dato corrupto tumbe todo el algoritmo, o que ese error se registre y el sistema siga operando con normalidad.