---
title: "0.3.5 Funciones: argumentos, lambdas y ámbito"
description: "Funciones de trading en Python: tamaño de posición con argumentos por defecto y por nombre, drawdown máximo, *args, **kwargs, lambdas y ámbito."
url: "https://pythonparatrading.com/curso-python-trading/nivel-0/funciones-argumentos-y-ambito/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-0/funciones-argumentos-y-ambito.png"
---

# 0.3.5 · Funciones: argumentos, *args y **kwargs, lambdas y ámbito

> Un cálculo que se repite se escribe una vez, con nombre, y se prueba. La lección construye funciones de trading —tamaño de posición por riesgo, drawdown máximo, ordenar operaciones— y muestra los dos errores clásicos de las funciones: el ámbito de las variables y el argumento por defecto mutable.

- Curso: [Nivel 0: Python desde cero y mentalidad de datos](https://pythonparatrading.com/curso-python-trading/nivel-0/)
- Módulo: 0.3 · Fundamentos del lenguaje
- Duración: 55 min
- Requisitos: [0.3.4 · Control de flujo: if, elif, else, for, while y match-case](https://pythonparatrading.com/curso-python-trading/nivel-0/control-de-flujo-if-for-while-match/)

## Al terminar esta lección

- Definir funciones con `def`, documentarlas y devolver uno o varios valores con `return`.
- Calcular el tamaño de una posición por riesgo con argumentos por defecto y argumentos solo por nombre.
- Aceptar un número variable de argumentos con `*args` y `**kwargs`, y ordenar operaciones por su P&L con `lambda`.
- Explicar el ámbito de una variable y evitar el error del argumento por defecto mutable.

## Por qué funciones

En las lecciones anteriores, cada cálculo se escribía donde se necesitaba. El trailing stop de la lección 0.3.4 se
escribió dos veces, una con el 5 % y otra con el 8 %, cambiando un número en dos sitios. Así empiezan los errores: se
cambia uno y se olvida el otro. Una **función** da nombre a un cálculo, lo escribe una sola vez y lo deja listo para
usarlo con datos distintos. Además, una función se puede **probar** por separado (lección 0.4.5): si el cálculo del
tamaño de posición está en una función, se comprueba una vez y se confía en él en todas partes.

Una función se define con `def`, su nombre, sus **parámetros** entre paréntesis y dos puntos. El cuerpo va sangrado, y
`return` devuelve el resultado a quien la llama:

```python
def pnl_operacion(entrada, salida, cantidad, lado="largo"):
    """P&L bruto de una operación cerrada.

    `lado`: "largo" (ganas si sube) o "corto" (si baja).
    """
    signo = 1 if lado == "largo" else -1
    return signo * (salida - entrada) * cantidad

print(pnl_operacion(102.83, 110.07, 200))
print(pnl_operacion(104.30, 111.86, 40, "corto"))
print(round(pnl_operacion(104.30, 111.86, 40, lado="corto"), 2))
```
```output
1447.999999999999
-302.4000000000001
-302.4
```

El texto entre triples comillas justo debajo del `def` es el **docstring**: la documentación de la función, que
muestran `help(pnl_operacion)` y los editores al pasar el ratón. Los valores que se pasan al llamarla son los
**argumentos**; se asignan a los parámetros por **posición** (el primero a `entrada`, el segundo a `salida`…) o por
**nombre** (`lado="corto"`). `lado="largo"` en la definición es un **valor por defecto**: si no se pasa, vale
`"largo"`. Las colas de decimales de las dos primeras líneas son la coma flotante de la lección 0.3.1; `round` las
quita al mostrar el resultado.

Una función sin `return` devuelve `None`. Es un error frecuente: el cálculo se hace, pero el resultado se pierde.

```python
def rentabilidad_sin_return(entrada, salida):
    r = salida / entrada - 1          # se calcula… y se pierde

resultado = rentabilidad_sin_return(102.83, 110.07)
print(resultado)
```
```output
None
```

## Tamaño de posición: argumentos por defecto y por nombre

El **tamaño de posición por riesgo** es la función más importante de este nivel. La idea: decidir cuánto dinero se
está dispuesto a perder si salta el stop (un porcentaje fijo del capital, el **riesgo por operación**) y comprar las
acciones que hacen que, en ese caso, la pérdida sea exactamente esa. Si se arriesga una fracción $f$ de un capital
$C$, con entrada $E$ y stop $S$:

$$
\text{acciones} = \left\lfloor \frac{f \cdot C}{E - S} \right\rfloor
$$

Los corchetes $\lfloor \cdot \rfloor$ indican redondeo hacia abajo: nunca se compra una acción de más, porque la
pérdida superaría el riesgo fijado. Si el activo se opera por lotes, se redondea hacia abajo al lote.

```python
def tamano_posicion(capital, entrada, stop, *, riesgo=0.01,
                    lote=1):
    """Acciones que arriesgan `riesgo` × `capital` hasta el stop.

    Solo posiciones largas: `stop` por debajo de `entrada`.
    """
    riesgo_por_accion = entrada - stop
    acciones = int(capital * riesgo / riesgo_por_accion)
    return acciones // lote * lote

n = tamano_posicion(25_000, 142.22, 137.95)
print(n, f"{n * (142.22 - 137.95):.2f}", f"{n * 142.22:,.2f}")
print(tamano_posicion(25_000, 142.22, 137.95, riesgo=0.02))
print(tamano_posicion(25_000, 142.22, 137.95, lote=10))
```
```output
58 247.66 8,248.76
117
50
```

Con 25.000 € y un 1 % de riesgo, la pérdida máxima es 250 €. La distancia al stop es 4,27 € por acción, así que caben
58 acciones: si salta el stop se pierden 247,66 €, y la posición vale unos 8.250 €, un tercio de la cuenta. `int()`
corta los decimales, que con números positivos es redondear hacia abajo, lo que aquí se busca; para un número de
ticks no lo era (lección 0.3.1), porque allí el resultado debía ser exacto.

El **asterisco** solo, `*`, en la lista de parámetros cambia las reglas: todo lo que va detrás solo se puede pasar
**por nombre**. Es una protección deliberada. Una función con cinco números seguidos es fácil de llamar mal, y
`tamano_posicion(25_000, 142.22, 137.95, 0.02)` no dice si 0,02 es un riesgo, una comisión o un lote. Python lo
rechaza:

```python raises
tamano_posicion(25_000, 142.22, 137.95, 0.02)
```
```output
TypeError: tamano_posicion() takes 3 positional arguments but 4 were given
```

La función tiene un hueco: si el stop está por encima de la entrada, la distancia es negativa y el resultado también.
Validar las entradas y avisar con una excepción es materia de la lección 0.3.6.

La relación entre stop y tamaño es inversa: cuanto más cerca el stop, más acciones. La figura de la cabecera la dibuja
con los números de la celda siguiente:

```python
for distancia in (0.01, 0.02, 0.03, 0.05, 0.08, 0.10):
    stop = round(142.22 * (1 - distancia), 2)
    n = tamano_posicion(25_000, 142.22, stop)
    print(f"stop a {distancia:>4.0%} ({stop:>6.2f}): {n:>3} acciones,"
          f" exposición {n * 142.22 / 25_000:>4.0%}")
```
```output
stop a   1% (140.80): 176 acciones, exposición 100%
stop a   2% (139.38):  88 acciones, exposición  50%
stop a   3% (137.95):  58 acciones, exposición  33%
stop a   5% (135.11):  35 acciones, exposición  20%
stop a   8% (130.84):  21 acciones, exposición  12%
stop a  10% (128.00):  17 acciones, exposición  10%
```

Con un stop al 1 %, el mismo riesgo de 250 € exige una posición del tamaño de toda la cuenta; de hecho algo mayor,
porque 176 acciones valen 25.031 €, y la función no lo impide. Un stop muy cercano no
reduce el riesgo, lo cambia de sitio: la pérdida prevista es la misma, pero la exposición es enorme, y un hueco de
apertura por debajo del stop (lección 0.3.4) se lleva mucho más de 250 €. El nivel 4 vuelve sobre el tamaño de
posición con volatilidad y con límites de exposición.

## Una función que recorre una serie: el drawdown máximo

El **drawdown** es la caída desde el máximo anterior; el **drawdown máximo**, la mayor de esas caídas en todo el
periodo. Es la medida de dolor de una estrategia (lección 0.1.2): cuánto llegó a perder quien entró en el peor
momento. Calcularlo exige recorrer la serie recordando el máximo visto hasta cada día:

```python
def drawdown_maximo(valores):
    """Mayor caída desde un máximo previo y sus posiciones.

    Devuelve (drawdown, posición del máximo, posición del mínimo).
    """
    pico, i_pico = valores[0], 0
    peor, i_ini, i_fin = 0.0, 0, 0
    for i, v in enumerate(valores):
        if v > pico:
            pico, i_pico = v, i
        caida = v / pico - 1
        if caida < peor:
            peor, i_ini, i_fin = caida, i_pico, i
    return peor, i_ini, i_fin

print(drawdown_maximo([100, 110, 99, 105, 120, 90, 130]))
```
```output
(-0.25, 4, 5)
```

Una función puede devolver varios valores separándolos con comas: lo que devuelve es una tupla, que se desempaqueta
al recibirla (lección 0.3.3). En la serie de prueba, la peor caída va de 120 (posición 4) a 90 (posición 5): un
−25 %. Probar una función con datos pequeños cuyo resultado se conoce de antemano es la costumbre que convierte el
código en algo fiable.

Con la función escrita y probada, aplicarla a los cierres de SINT es una línea:

```python
import csv

with open("sint_diario.csv", encoding="utf-8") as f:
    filas = list(csv.DictReader(f))
fechas = [r["fecha"] for r in filas]
cierres = [float(r["cierre"]) for r in filas]

dd, i_ini, i_fin = drawdown_maximo(cierres)
print(f"Drawdown máximo {dd:.1%}: de {cierres[i_ini]:.2f} el "
      f"{fechas[i_ini]} a {cierres[i_fin]:.2f} el {fechas[i_fin]}")
```
```output
Drawdown máximo -25.0%: de 125.98 el 2022-03-07 a 94.45 el 2022-10-20
```

Del máximo de marzo de 2022 al mínimo de octubre, SINT cae un 25 %. Según la cuenta de la lección 0.1.2, recuperar
esa caída exige después una subida del 33,3 %. La lección 0.6.1 dibuja esta serie y su drawdown completo.

## Argumentos variables: *args y **kwargs

Algunas funciones no saben de antemano cuántos argumentos van a recibir. Un parámetro con un asterisco, `*args`,
recoge todos los argumentos posicionales que sobren en una **tupla**; uno con dos, `**kwargs`, recoge los que se pasan
por nombre y no tienen parámetro propio en un **diccionario**. Los nombres `args` y `kwargs` son una convención:
lo que importa son los asteriscos.

```python
def coste_total(*comisiones, minimo=1.0):
    """Suma comisiones; ninguna por debajo del mínimo."""
    return round(sum(max(c, minimo) for c in comisiones), 2)

print(coste_total(5.14, 0.42, 7.10))
print(coste_total(5.14, 0.42, 7.10, minimo=0.0))

def crear_orden(simbolo, cantidad, **opciones):
    """Orden de mercado por defecto; el resto, por nombre."""
    orden = {"simbolo": simbolo, "cantidad": cantidad,
             "tipo": "mercado"}
    orden.update(opciones)
    return orden

print(crear_orden("SINT", 58))
print(crear_orden("SINT", 58, tipo="limite", precio=141.50,
                  validez="dia"))
```
```output
13.24
12.66
{'simbolo': 'SINT', 'cantidad': 58, 'tipo': 'mercado'}
{'simbolo': 'SINT', 'cantidad': 58, 'tipo': 'limite', 'precio': 141.5, 'validez': 'dia'}
```

`max(c, minimo) for c in comisiones` dentro de `sum` es una comprensión sin corchetes (una **expresión generadora**,
que se ve en la lección 0.4.3). `update` añade al diccionario de la orden las parejas de otro diccionario,
sustituyendo las claves que ya existan: por eso `tipo="limite"` reemplaza al tipo por defecto.

Los asteriscos también funcionan al revés, al **llamar**: `*lista` reparte una secuencia en argumentos posicionales y
`**diccionario` reparte un diccionario en argumentos por nombre. Es la forma de guardar la configuración de una
estrategia en un diccionario y pasársela a una función:

```python
comisiones_dia = [5.14, 0.42, 7.10, 2.00]
print(coste_total(*comisiones_dia))

config = {"riesgo": 0.005, "lote": 5}
print(tamano_posicion(25_000, 142.22, 137.95, **config))
```
```output
15.24
25
```

Úsalos con mesura. Una función con `**kwargs` acepta cualquier nombre, también uno mal escrito: `crear_orden("SINT",
58, presio=141.50)` crea una orden con una clave `presio` que nadie va a leer, sin ningún aviso. Cuando los parámetros
se conocen, es mejor escribirlos.

## Lambdas: funciones de una línea

Una **lambda** es una función sin nombre y de una sola expresión: `lambda op: op["pnl"]` equivale a una función que
recibe `op` y devuelve `op["pnl"]`. Su uso principal es el argumento `key` de `sorted`, `max` y `min`, que dice
**por qué** comparar los elementos. Con las 40 operaciones de `operaciones_sint.csv`:

```python
with open("operaciones_sint.csv", encoding="utf-8") as f:
    ops = list(csv.DictReader(f))
for op in ops:
    bruto = pnl_operacion(float(op["precio_entrada"]),
                          float(op["precio_salida"]),
                          int(op["cantidad"]), op["lado"])
    op["pnl"] = round(bruto - float(op["comision"]), 2)

por_pnl = sorted(ops, key=lambda op: op["pnl"])
for op in por_pnl[:2] + por_pnl[-2:]:
    print(op["id"], op["simbolo"], op["lado"], f"{op['pnl']:+,.2f}")

mayor = max(ops, key=lambda op: int(op["cantidad"]))
print("Mayor tamaño:", mayor["id"], mayor["cantidad"])
```
```output
1 ORO corto -3,123.56
8 SINT largo -1,288.04
13 SINT largo +1,011.70
7 SINT largo +1,426.71
Mayor tamaño: 1 200
```

`sorted` devuelve las operaciones completas, ordenadas de menor a mayor P&L; `por_pnl[:2]` son las dos peores y
`por_pnl[-2:]`, las dos mejores. `max` con `key` devuelve el elemento entero, no el valor de la clave. `pnl_operacion`,
la función del principio de la lección, se reutiliza tal cual: para eso se escribió.

Una lambda que se asigna a un nombre (`riesgo = lambda c: c * 0.01`) es una mala idea: para eso está `def`, que da un
nombre visible en los errores y admite docstring.

## Ámbito: dónde vive cada variable

Las variables que se asignan dentro de una función son **locales**: existen solo mientras la función se ejecuta y no
afectan a las de fuera aunque se llamen igual. Para leer un nombre, Python lo busca en cuatro sitios, en este orden
(la regla LEGB):

```mermaid
flowchart LR
  L[Local] --> E[Función externa] --> G[Global] --> B[Integrados]
```

Primero en la función (local), después en la función que la contiene si la hay (*enclosing*), después en el módulo
(global) y por último en los nombres integrados de Python (`print`, `len`, `max`…). La consecuencia práctica:

```python
capital = 25_000

def capital_tras_perdida(perdida):
    capital = 25_000 - perdida      # local: otra variable
    return capital

print(capital_tras_perdida(250), capital)
```
```output
24750 25000
```

La función tiene su propio `capital`, y el global sigue intacto. Si dentro de la función se intenta **modificar** la
variable global a partir de sí misma, Python da un error, porque una asignación hace local el nombre en toda la
función, también en la línea que intenta leerlo antes:

```python raises
def pagar_comision(comision):
    capital -= comision            # lee y asigna: es local

pagar_comision(5.14)
```
```output
UnboundLocalError: cannot access local variable 'capital' where it is not associated with a value
```

Existe la palabra `global` para saltarse esta regla, y no conviene usarla: una función que cambia variables de fuera
es difícil de probar y de seguir. La forma limpia es recibir el valor como argumento y devolver el nuevo:
`capital = pagar_comision(capital, 5.14)`.

## El argumento por defecto mutable

El último error es el más traicionero porque no da ningún mensaje. El valor por defecto de un parámetro se evalúa
**una sola vez**, al definir la función, no en cada llamada. Si ese valor es una lista o un diccionario, todas las
llamadas comparten el mismo objeto (la trampa de «asignar no es copiar» de la lección 0.3.3):

```python
def registrar_orden(orden, libro=[]):
    libro.append(orden)
    return libro

dia_1 = registrar_orden({"simbolo": "SINT", "cantidad": 58})
dia_2 = registrar_orden({"simbolo": "ORO", "cantidad": 20})
print(len(dia_2), dia_1 is dia_2)
print([o["simbolo"] for o in dia_2])
```
```output
2 True
['SINT', 'ORO']
```

El segundo día empieza con la orden del primero dentro. En un sistema real, la orden de ayer se volvería a enviar. La
solución es usar `None` como valor por defecto y crear la lista dentro, en cada llamada:

```python
def registrar_orden(orden, libro=None):
    if libro is None:
        libro = []
    libro.append(orden)
    return libro

dia_1 = registrar_orden({"simbolo": "SINT", "cantidad": 58})
dia_2 = registrar_orden({"simbolo": "ORO", "cantidad": 20})
print(len(dia_1), len(dia_2), dia_1 is dia_2)
```
```output
1 1 False
```

La regla: nunca un `[]`, `{}` o `set()` como valor por defecto. Números, cadenas, tuplas, `True`, `False` y `None` son
inmutables y no tienen el problema.

## Ejercicios

1. Escribe `rentabilidades(cierres)`, que devuelva la lista de rentabilidades diarias de una lista de cierres (usa la
   comprensión de la lección 0.3.3), y pruébala con `[100, 110, 99]`. Después calcula cuántas sesiones de SINT
   bajaron más de un 3 %.

**Solución.**

```python
def rentabilidades(cierres):
    """Rentabilidad de cada cierre respecto al anterior."""
    return [c1 / c0 - 1 for c0, c1 in zip(cierres, cierres[1:])]

print([round(r, 4) for r in rentabilidades([100, 110, 99])])
caidas = [r for r in rentabilidades(cierres) if r < -0.03]
print(len(caidas))
```
```output
[0.1, -0.1]
11
```

La prueba con tres números confirma la función (+10 % y −10 %) antes de usarla con los 1.303 cierres de SINT.

2. Amplía `tamano_posicion` con un parámetro solo por nombre `max_exposicion=1.0` (fracción del capital) que limite el
   valor de la posición. Con un stop al 1 % (140,80), ¿cuántas acciones da con `max_exposicion=0.5`?

**Solución.**

```python
def tamano_posicion(capital, entrada, stop, *, riesgo=0.01,
                    lote=1, max_exposicion=1.0):
    """Acciones por riesgo, con un tope de exposición."""
    por_riesgo = int(capital * riesgo / (entrada - stop))
    por_exposicion = int(capital * max_exposicion / entrada)
    acciones = min(por_riesgo, por_exposicion)
    return acciones // lote * lote

print(tamano_posicion(25_000, 142.22, 140.80))
print(tamano_posicion(25_000, 142.22, 140.80, max_exposicion=0.5))
```
```output
175
87
```

Sin pasar el tope, la función ya da 175 acciones y no las 176 de antes: con el valor por defecto, 1.0, la posición
no puede valer más que la cuenta. Con `max_exposicion=0.5`, 87. El tope de exposición manda cuando el stop es muy
cercano: el riesgo por operación sigue por debajo de 250 €, pero la
posición ya no puede comprometer más de la mitad de la cuenta. Como el parámetro tiene valor por defecto, las
llamadas antiguas siguen funcionando igual.

3. Con la lista `ops` de la lección, usa `sorted` y una lambda para obtener las tres operaciones con mayor pérdida en
   porcentaje sobre el valor de entrada (P&L entre precio de entrada por cantidad).

**Solución.**

```python
def pct(op):
    return op["pnl"] / (float(op["precio_entrada"])
                        * int(op["cantidad"]))

for op in sorted(ops, key=pct)[:3]:
    print(op["id"], op["simbolo"], f"{pct(op):+.2%}",
          f"{op['pnl']:+,.2f}")
```
```output
8 SINT -10.16% -1,288.04
1 ORO -8.22% -3,123.56
4 SINT -7.35% -306.72
```

Cuando el cálculo de la clave no cabe con claridad en una lambda, una función con `def` sirve igual como `key`. Las
peores operaciones en porcentaje no tienen por qué ser las peores en euros: dependen del tamaño de cada una.

## Autoevaluación

1. Una función `def tamano(capital, entrada, stop, *, riesgo=0.01)` se llama con `tamano(25_000, 142.22, 137.95, 0.02)`. ¿Qué pasa?

   - A. Usa un riesgo del 2 %
   - B. Usa un riesgo del 1 % e ignora el 0,02
   - C. Da un `TypeError`: `riesgo` solo se puede pasar por nombre
   - D. Usa 0,02 como stop

   *Respuesta C: Da un `TypeError`: `riesgo` solo se puede pasar por nombre.* Lo que va detrás de `*` en la definición solo se puede pasar por nombre (`riesgo=0.02`). Así nadie confunde el riesgo con otro número por su posición.

2. ¿Qué devuelve una función que calcula un resultado pero no tiene `return`?

   - A. El último valor calculado
   - B. `None`
   - C. `0`
   - D. Da un error al llamarla

   *Respuesta B: `None`.* Sin `return`, una función devuelve `None`. Es un error típico: el cálculo se hace, pero el valor se pierde y quien la llama recibe `None`.

3. `def registrar(orden, libro=[])` añade la orden a `libro` y lo devuelve. Tras llamarla dos veces sin `libro`, ¿cuántas órdenes tiene la lista de la segunda llamada?

   - A. 1
   - B. 2: la lista por defecto se crea una sola vez y se comparte entre llamadas
   - C. 0
   - D. Da un error

   *Respuesta B: 2: la lista por defecto se crea una sola vez y se comparte entre llamadas.* El valor por defecto se evalúa una vez, al definir la función. Las dos llamadas usan la misma lista, que va acumulando órdenes. La solución es `libro=None` y crear la lista dentro.

4. ¿Qué hace `sorted(operaciones, key=lambda op: op["pnl"])`?

   - A. Ordena las operaciones por su P&L, de menor a mayor
   - B. Devuelve solo los P&L ordenados
   - C. Ordena por el nombre del símbolo
   - D. Modifica la lista `operaciones`

   *Respuesta A: Ordena las operaciones por su P&L, de menor a mayor.* `key` recibe una función que se aplica a cada elemento para saber por qué ordenar. La lista que devuelve tiene las operaciones completas, y la original no cambia.

## Ideas clave

- Una función da nombre a un cálculo: `def`, parámetros, docstring y `return`. Sin `return`, devuelve `None`.
- Los parámetros con valor por defecto se pueden omitir; los que van tras `*` solo se pasan por nombre, lo que evita confundir riesgo, lote o comisión por su posición.
- `*args` recoge argumentos posicionales sueltos en una tupla y `**kwargs` los que van por nombre en un diccionario.
- `lambda` es una función de una expresión, útil como `key` de `sorted`, `max` y `min`.
- Las variables asignadas dentro de una función son locales. Un valor por defecto mutable (`[]`, `{}`) se comparte entre llamadas: usa `None`.

## Recursos

### Research

- [Volatility Targeting en Python](https://pythonparatrading.com/volatility-targeting/) · Research-0004 · 8 min

### Documentación

- [Python: definir funciones (tutorial oficial)](https://docs.python.org/3/tutorial/controlflow.html#defining-functions) · docs.python.org
- [Python: guía de ordenación, funciones `key` (documentación oficial)](https://docs.python.org/3/howto/sorting.html) · docs.python.org
- [Python: ámbitos y espacios de nombres (tutorial oficial)](https://docs.python.org/3/tutorial/classes.html#python-scopes-and-namespaces) · docs.python.org

### Datos

- [Datos del nivel 0: 40 operaciones ficticias (CSV)](https://pythonparatrading.com/curso/datos/nivel-0/operaciones_sint.csv) · Descarga · operaciones_sint.csv

Lección anterior: [0.3.4 · Control de flujo: if, elif, else, for, while y match-case](https://pythonparatrading.com/curso-python-trading/nivel-0/control-de-flujo-if-for-while-match/)

Lección siguiente: [0.3.6 · Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15](https://pythonparatrading.com/curso-python-trading/nivel-0/excepciones-modulos-y-paquetes/)
