El código se lee mucho más de lo que se escribe
Una base de código de trading vive años y la modifican personas que no la escribieron, a menudo bajo presión y a menudo tú mismo seis meses después. El código limpio es el que se entiende rápido y se puede cambiar con seguridad. No trata de reglas de estilo, sino de hacer visible un cambio equivocado. Una función mal nombrada, que hace tres cosas y esconde un límite dentro de una fórmula, es donde viven los errores caros, como un límite de riesgo que nadie recordaba que estaba fijo en el código.
# The same calculation, written two ways
def f(d, e, m, w):
r = []
if sum([abs(x) for x in d.values()]) / e > m:
r.append("LEVERAGE")
for k in sorted(d):
if abs(d[k]) / sum([abs(x) for x in d.values()]) > w:
r.append("CONCENTRATION:" + k)
return r
book = {"AAPL": 80_000.0, "MSFT": -10_000.0, "NVDA": 10_000.0}
print(f(book, 50_000.0, 1.5, 0.5)) # works, but what is d? e? m? w?Nombres, funciones pequeñas, sin números ocultos
Tres hábitos hacen la mayor parte. Nombra las cosas por lo que son: gross_exposure en lugar de f, max_leverage en lugar de m. Mantén las funciones pequeñas y de un solo propósito: cada una hace un trabajo que se nombra en pocas palabras, así se prueba sola y se reutiliza, y una función mayor se construye llamándolas. No entierres números: un límite como 1.5 escrito dentro de una fórmula es un número mágico que nadie puede encontrar ni cambiar con seguridad; pásalo como argumento (o ponle nombre de constante). Calcular la exposición bruta una vez en su propia función también elimina la expresión repetida.
# Small functions with names that say what they return; the limits come in as arguments
def gross_exposure(positions):
return sum(abs(value) for value in positions.values())
def leverage(positions, equity):
return gross_exposure(positions) / equity
def risk_flags(positions, equity, max_leverage, max_weight):
flags = []
if leverage(positions, equity) > max_leverage:
flags.append("LEVERAGE")
gross = gross_exposure(positions)
for symbol in sorted(positions):
if abs(positions[symbol]) / gross > max_weight:
flags.append(f"CONCENTRATION:{symbol}")
return flags
book = {"AAPL": 80_000.0, "MSFT": -10_000.0, "NVDA": 10_000.0}
print(risk_flags(book, 50_000.0, 1.5, 0.5)) # ['LEVERAGE', 'CONCENTRATION:AAPL']
print(risk_flags(book, 50_000.0, 3.0, 0.9)) # [] the same code, looser limitsProteger los bordes y decir por qué
Trata primero la entrada mala y devuelve o lanza de inmediato (una cláusula de guarda), para que el camino principal se lea recto, sin anidar. Un apalancamiento gross / equity con capital cero debe ser un ValueError claro, no un ZeroDivisionError desde el fondo, y una cartera vacía debe dar 0.0 y no un fallo. Los comentarios deben explicar el porqué, que el código no puede decir (# la bolsa redondea al centavo), no repetir el qué que el código ya muestra. Las docstrings dicen qué recibe y devuelve una función. Por último, prefiere las funciones puras, que dependen solo de sus argumentos y no tienen efectos secundarios: son las que se prueban con unas pocas entradas.
# A guard clause stops bad input at the door and says exactly what is wrong
def leverage(positions, equity):
if equity <= 0:
raise ValueError("equity must be positive")
return sum(abs(value) for value in positions.values()) / equity
print(leverage({"AAPL": 60_000.0, "MSFT": -30_000.0}, 45_000.0)) # 2.0
print(leverage({}, 45_000.0)) # 0.0 an empty book is fine
try:
leverage({"AAPL": 60_000.0}, 0.0)
except ValueError as error:
print("rejected:", error) # rejected: equity must be positive# A pure function answers the same way every time; hidden state does not
history = []
def running_notional(price, quantity): # remembers earlier calls: the answer depends on the past
history.append(price * quantity)
return sum(history)
def notional(price, quantity): # depends only on its arguments
return price * quantity
print(running_notional(10.0, 5), running_notional(10.0, 5)) # 50.0 100.0 same call, different answers
print(notional(10.0, 5), notional(10.0, 5)) # 50.0 50.0 always the same- Nombra las cosas por lo que son, mantén las funciones pequeñas y de un solo propósito, y construye las mayores llamándolas.
- Sin números mágicos: los límites llegan como argumentos o constantes con nombre; calcula una vez un valor repetido.
- Protege primero contra la entrada mala con un error claro, comenta el porqué y no el qué, y prefiere funciones puras que puedas probar.
A continuación: anotaciones de tipo y comprobación estática, dejar que las herramientas detecten errores antes de ejecutar el código.
Lab: qué se califica
- Netear la cartera con
net_exposure25 pts - Sumar cada posición con
gross_exposure25 pts - Proteger el denominador en
leverage25 pts - Componer las comprobaciones en
risk_flags25 pts