Backtesting y Comisiones

La expresión inglesa “nickel and diming” representa muy bien el impacto de las comisiones de intermediación en un sistema: un euro aquí, un euro allá, esquilmando los beneficios obtenidos un día sí y otro también… de este modo, prácticamente sin darnos cuenta, el rendimiento de nuestro sistema se derrumba como un tronco carcomido por las termitas.

Tal vez deprimente, pero nada ganamos con sustentar castillos en el aire. Nuestro backtesting sólo es tan bueno como su capacidad de reproducir la realidad. Posiblemente lo pasaremos de maravilla en en el mundo fantástico de Pin y Pon, pero sólo ganaremos dinero en los mercados financieros reales, con sangre, sudor, y lágrimas ocasionales. Así que vamos adelante con la tarea de cargarnos nuestro sistema.

Comisiones

El primer factor de fricción que vamos a introducir son las comisiones que su broker le va a cobrar cada vez que quiera operar. Las estructuras de estas comisiones son de lo más variopinto: un coste fijo, un porcentaje del valor, una cantidad mínima… la belleza de un sistema abierto como PyAlgoTrade es que podemos modelar a nuestro Broker tal y como necesitemos para replicar las complejidades a las que nos enfrentamos.

En lugar de crear directamente nuestra estrategia con el presupuesto inicial, instanciaremos un Broker personalizado con las Comisiones oportunas. Las que vienen por defecto con el entorno son:

  • NoComission: la predeterminada, sin comisiones.
  • FixedPerTrade: coste fijo por operación.
  • TradePercentage: un porcentaje del valor.

Una comisión razonable para un minorista que acude a un broker español se encuentra hoy en día alrededor de un 0.20% del valor. Así que utilizaremos ese criterio para testar nuestro sistema anterior que incorpora indicadores SMA y RSI sobre el IBEX 35.

from pyalgotrade.broker import backtesting as broker

myBroker = broker.Broker(1000000, feed, broker.TradePercentage(0.002))
strategy.BacktestingStrategy.__init__(self, feed, myBroker)

Les dejo en aquí el código completo den dos archivos: Ibex2010RsiCosts.py para la parte sólo con largos. Al ejecutarlo veremos que, en efecto, nuestra soñada rentabilidad desaparece como el humo: la rentabilidad acumulada cae al 20.19%, y junto con ella también se esfuma el ratio de Sharpe, marcando un decepcionante 27.23 (lógico, debido al hecho de que se reduce la rentabilidad mientras que la volatilidad aumenta ligeramente con las comisiones).

Reduciendo Operativa

¿Es esto el último clavo sobre el ataúd del sistema? Puede ser, pero detengámonos a experimentar un poco más.  Tal y como está ahora nuestra estrategia tiene un perfil claramente de corto plazo: produce muchas entradas y salidas que incrementan tremendamente la frecuencia de la operativa. Esto se exacerba con la presencia de salida sobre el SMA de 5 días, que provoca salidas aún más veloces.

Podríamos reemplazar esto con una orden de stop relacionada con los precios, tal vez de tipo trailing (a la zaga de la evolución del activo) o bien un SAR parabólico. Estas son vías de investigación importantes que no deberíamos subestimar, no en vano y en contra de lo que piensan muchos analistas, los elementos más importantes de un sistema exitoso son las salidas, no las entradas. Podemos ganar dinero con un sistema mediocre incluso si tiene entradas aleatorias, mientras incorpore robustos algoritmos de salida.

Pero esos son temas demasiado avanzados para nuestro punto en el recorrido, y temo ahogar a mi querido lector con demasiados torrentes antes de que hayamos aprendido bien a nadar. Así que dejémoslos para más adelante y concentrémonos ahora en lo más importante: reducir la frecuencia. Un gran responsable de dicho problema es el RSI: su corta frecuencia, de sólo 2 sesiones, hace que oscile muy rápidamente y las bandas tan amplias se sobrepasan con extrema facilidad. Si representamos gráficamente el RSI de un título en cartera (por ejemplo, Ferrovial) veremos lo siguiente:

plt.getOrCreateSubplot("RSI").addDataSeries("RSI", myStrategy._rsis["FER.MC"])
myStrategy.run()

FER_RSI_1

Vamos a probar con un periodo de 5 sesiones y reduciendo la banda de sobreventa a 15. Ejecute el código en el archivo Ibex2010RsiLow.py o produzca el suyo propio, el cambio es bastante notable:

FER_RSI_2

Respecto a los resultados, experimentamos un impacto similar en la rentabilidad acumulada, que se reduce a 20.20%. Pero mire lo que sucede con el ratio de Sharpe: 98.46, estelar. ¿Cómo puede ser? Esto se debe a la bajísima volatilidad de la metodología, escasamente 2.6%. Nuestra reducción de frecuencia ha creado un sistema muy estable en términos de riesgo y pérdida máxima.

¿Qué sucede ahora si introducimos los cortos, de forma incluso más conservadora, a una banda de 95? Lo puede comprobar usted directamente en Ibex2010RsiShortLow.py: nuestra rentabilidad aumenta al 21.58% y el ratio de Sharpe alcanza 103.52. De nuevo los cortos introducen estabilidad en el sistema. Puede que la cantidad de beneficios no se ajuste a nuestro objetivo o perfil inversor, y que queramos introducir apalancamiento para explotar el bajo riesgo, o cambiar otros elementos, pero en términos de retorno ajustado al riesgo obtenemos resultados sólidos.

Comisiones Personalizadas

La flexibilidad en modelar comisiones resulta particularmente útil cuando nuestra estrategia no sólo es multiactivo, sino también multimercado. Por ejemplo: ¿Sabía usted que si decide invertir en activos de la bolsa de Londres sin que su vehículo esté radicado en suelo británico, va a tener que pagar un 0.5% adicional en la operación de compra? Este sablazo de la reina recibe el nombre de stamp duty y puede cortar en seco sus aspiraciones de rentabilidad si no lo tiene en cuenta. Otras plazas mundiales cuentan con imposiciones similares.

Vamos a imaginar que, además del elenco nacional, operamos con tres compañías del FTSE: BP, iSystems y Pearson Plc. En este ejemplo pagamos a nuestro broker un 0.15% sobre el valor de la operación. Si se trata de una venta de activos españoles, la comisión mínima es de 25 euros. Si se trata de una compra de activos españoles hemos de añadir 5 euros de comisión fija, sin mínimos. Finalmente, en el caso de que se trate de una compra de activos británicos, contabilizamos el stamp duty de 0.5% del valor para su majestad.

gb_instruments = ["BP.L", "ISYS.L", "PSON.L"]

class MyCommission(broker.Commission):
    def calculate(self, order, price, quantity):
        commission = 0.0015 * price * quantity
        action = order.getAction();
        isBuy = False
        if action == Order.Action.BUY or action == Order.Action.BUY_TO_COVER:
            isBuy = True
        instrument = order.getInstrument()
        if instrument in gb_instruments:
            if isBuy:
                commission += (0.005 * price * quantity)
        else:
            if isBuy:
                commission += 5
            elif commission < 25:
                commission = 25
        return commission

Cada fricción nos aleja más del nirvana financiero… pero mejor saberlo antes que después.

nickel_dime

Deslizamiento

En lugar de suponer que siempre obtenemos el “precio perfecto” de las series históricas, otro elemento de fricción que permite introducir realismo en nuestro sistema es el deslizamiento (slippage, en inglés), definido como la diferencia que se produce entre entre el precio al que colocamos nuestra orden a stop en el mercado y el precio al que realmente se ejecuta la orden.

El deslizamiento es notoriamente difícil de estimar, y las clases predeterminadas que vienen con PyAlgoTrade no son muy satisfactorias: NoSlippage por defecto no cuenta con ningún deslizamiento mientras que VolumeShareSlippage modela el deslizamiento de manera inteligente utilizando como base el volumen de situación, pero nos creará problemas si carecemos de esa información en nuestras barras.

Una solución de compromiso puede ser suponer que el mercado siempre se mueve en contra nuestra (arriba al comprar, abajo al vender) tanto en corto como en largo por un porcentaje fijo del valor en curso. Una implementación sencilla podría ser:

from pyalgotrade.broker import Order, slippage, backtesting as bbroker

class MySlippage(slippage.SlippageModel):
    def calculatePrice(self, order, price, quantity, bar, volumeUsed):
        action = order.getAction()
        if action == Order.Action.BUY or action == Order.Action.BUY_TO_COVER:
            price += price * 0.0005
        else:
            price -= price * 0.0005
        return price
myBroker.getFillStrategy().setSlippageModel(MySlippage())

Le dejo aquí el archivo Ibex2010Slippage.py para que compruebe los efectos, bastante moderados, del deslizamiento sobre nuestro sistema de ejemplo.

Por último, si dispone de tiempo extra, y quiere ganar bonus points no deje de explorar esa clase raíz del deslizamiento: FillStrategy. Es ella la que gobierna la forma en la que el broker se encarga de cumplimentar nuestras solicitudes de operación en los mercados, y le puede servir para representar escenarios realmente avanzados.

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s