Quant500 Academy

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 orden

Para 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 ganancia

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

El resto de Quant500

Lo que estás aprendiendo, aplicado al índice todos los días:

La cartera del modelo, cada semana en tu correo

Cada sábado, su semana en un minuto: cómo fue, lo mejor y lo peor, y qué directivos compraron con su dinero. Y el día que el modelo se rehace, qué entra y qué sale. Es una simulación, no un consejo. Como mucho un correo a la semana, sin publicidad y sin cuenta; te das de baja con un clic. Lo hace una persona: Pol Gracia.

Cartera modelo con resultados simulados, no una recomendación personalizada. Tu correo se usa solo para esto: privacidad.

Detrás de la Academia y del resto de Quant500 hay una sola persona: Pol Gracia Serrahima.