Invertir en bolsa

¿Como invertir en bolsa usando un simulador o programa automático?

Uno de los mayores problemas a la hora de hacer nuestro trading radica en el lapso de tiempo en el que debemos tomar los parámetros necesarios para una equity exitosa. Este lapso de tiempo donde adquirimos los parámetros lo denominamos comúnmente periodo “in-sample”, “dentro de la muestra”. Es fácil; hacemos nuestras pruebas desde una fecha inicial en el pasado a una fecha  final (también perteneciente al pasado) y calculamos lo que queramos.

Ahora bien, con estos parámetros matemáticos, estadísticos, cuantitativos, como queramos decirlo, debemos aplicarlos a un período de tiempo posterior a la fecha final del periodo “in-sample”. Al aplicar estos parámetros del pasado sobre una serie de tiempo posterior, denominamos a esta serie posterior periodo “out-sample” “fuera de la muestra”,  que es donde calcularemos una equity más real y que nos aportará más información para el desarrollo de nuestra estrategia.

 

En este artículo vamos a trabajar una pareja de currencies, “NZDUSD”  (dólar neozelandés-dólar americano) y “USDCHF (dólar americano-franco suizo)”.  Vamos a estudiar su cointegración en el pasado (periodo in-sample) y vamos a aplicar los parámetros obtenidos  a un periodo de tiempo futuro (out-sample) “y pasado”. ¿ Diras que por qué  “y pasado” ?. Bueno, pues para darnos cuenta cómo varía una simulación de nuestra estrategia de un periodo donde hemos calculado nuestros parámetros, a un periodo “nuevo” out-sample

 

1

Para nuestras pruebas vamos a usar los siguientes parámetros:

1) Método de búsqueda de pesos para la cointegración: Mediante test de Johansen.

Mediante Johansen podemos conocer el grado de cointegración de las variables que forman el sistema (NZDUSD y USDCHF), además de proporcionarnos unos pesos o betas de acuerdo al grao de cointegración. Lo bueno de usar Johansen es que los pesos salen ajustados y no necesitamos conocer el valor del pip, únicamente tendremos que hacer ciertos ajustes en la data.

delete(timerfind); % Borra todos los timers de memoria
clear all;
import dukasAPI.*;

instrumentos = {‘NZDUSD’,’USDCHF’};
instrumentosJava = javaArrayList(instrumentos);
timeFrameStr=’DAILY’;

conexion = DukasConnection(‘myUsername’,’myPasswd’);
conexion.conectar();

operacion = HistoryBarsSynchv2(instrumentosJava); %crea un objeto Java
conexion.startStrategy(operacion); %operacion tiene las funciones necesarias para interactuar con los datos que necesitamos

lookback=10; %configurado manualmente
myTableInSample = getDataFromDukasv2(operacion,instrumentos,timeFrameStr,’11-10-2002′,’31-12-2014′);

 

 

2) Periodos:

in-sample: desde 11-10-2002 – 31-12-2014

out-sample: desde 01-01-2015 – 23-07-2015

 

3)Time Frame: Diario

Usaremos un time frame de datos diario,

4) Lookback:10 periodos

Vamos a usar una lookback de 10 periodos para facilitar las cosas.  Diferentes estudios aconsejan utilizar lookback iguales al Half-Life pero no siempre esto es lo correcto, ya que usando lookbacks altos, aumenta el rango del zscore  y la también la kurtosis del spread.  Es muy habitual utilizar lookbacks establecidos manualmente para que el zscore se mantenga en un rango de +-2. En nuestro caso una lookback de 10 nos mantendrá en un zscore de +-2.8 aproximadamente. Normalmente a medida que aumenta la lookback, el drow down de nuestra equity disminuye, pero disminuye también la frecuencia de oportunidades.  Como siempre, la sincronía entre el número de operaciones y el beneficio por operación es algo que solo el trader es capaz de ajustar de acuerdo a su sesgo; no hay parámetros óptimos; cada uno tiene los suyos.

” HistoryBarsSynchv2″ es una clase de la librería dukasAPI creada para interactuar con los datos del broker.  ” getDataFromDukasv2″ es una función (método) de la clase  ” HistoryBarsSynchv2″ y permite obtener data histórica en tiempo real sobre cualquier par bien en intervalo de tiempo o bien por número de barras según el time frame. En este punto tendremos una tabla con los datos in-sample en la fecha dada. Hemos utilizado una serie de funciones compiladas en Java con Netbeans para interactuar con DukasCopy que dispone de una interfaz Java muy amplia y robusta.

 

2
Vamos a ajustar la data de nuestros 2 pares de forma que tengamos al “USD” como referencia común, es decir:
USDCHF = 1/CHFUSD
Para ello:
nzdusd = myTableInSample(:,3);
nzdusd = table2array(nzdusd); %convierte a array
usdchf = myTableInSample (:,4);
usdchf = table2array(usdchf); %convierte a array
chfusd =1./ usdchf;

Calculamos el test de Johansen y comprobamos la correlación. Para ello, vamos a usar todo el periodo in-sample para calcular los pesos. En otro artículo mostraremos como aumentar la fuerza del test de johansen y buscar valores de cointegración mucho más fuertes mediante un algoritmo de búsqueda de los mejores “estimates”

johansenGanador=johansen([nzdusd chfusd ], 0, 1);

>> prt(johansenGanador)
Johansen MLE estimates
NULL: Trace Statistic Crit 90% Crit 95% Crit 99%
r <= 0 variable 1 15.863 13.429 15.494 19.935
r <= 1 variable 2 3.734 2.705 3.841 6.635

NULL: Eigen Statistic Crit 90% Crit 95% Crit 99%
r <= 0 variable 1 12.129 12.297 14.264 18.520
r <= 1 variable 2 3.734 2.705 3.841 6.635

El resultado del test nos muestra el grado de cointegración de las variables. Para verificar que estamos en un grado alto de cointegración, debemos fijarnos en la estadística de los EigenVectors; en nuestro caso, la variable “nzdusd” con valor 12.129 está próxima al valor del 90% de cointegración 12.297. A su vez, la segunda variable “chfusd” con valor 3.734 está cerca del 95% de cointegración 3.841. Por tanto, estamos en unos valores adecuados para pensar que ambas variables están cointegradas y operaremos en función de ello.
Para obtener los pesos (unidades, lotes, microlotes, betas) que conforman la compra/venta de los pares dentro de la cartera usaremos el siguiente comando:
>> johansenGanador.evec
ans =
19.4199 -2.4670 %19.4199 -> lotaje correspondiente al nzdusd
-9.7712 8.5409 % -9.7712 -> lotaje correspondiente al usdchf

¿ Y qué significa todo esto ?. Bien, pues si nuestro zscore, que calcularemos posteriormente, nos marca una “compra” (ha llegado al -80% , por ejemplo, del zscore y vamos largos en el spread) entraremos al mercado de esta forma:
• Buy 19.41 unidades de nzdusd
• Sell 9.77 unidades de chfusd = Buy 9.77 unidades de usdchf

Si el zscore marca una “venta” (ha llegado al +80% , por ejemplo, del zscore y vamos cortos en el spread) entraremos al mercado multiplicando por -1 los pesos, de esta manera:
• Sell 19.41 unidades de nzdusd
• Buy 9.77 unidades de chfusd = Sell 9.77 unidades de usdchf

¿Y siempre con la misma cantidad?
Pues eso varía de nuestro modelo, una vez tenemos la proporción de peso dado por los eigenvectors, nuestra tarea es ajustar la proporción al tamaño de nuestra cartera, en función del margen requerido por operación y apalancamiento. Si podemos ir con 19.41 y 9.77 lotes, pues adelante, pero como eso puede suponer una barbaridad, pues podemos ajustar en 1.94 y 0.97 lotes que es algo más suave. Debemos tener también presente que a medida que aumente la separación de pesos, más responsabilidad caerá en una sola de nuestras variables. En nuestro caso nzdusd tiene casi el doble de peso que usdchf.

Observemos que no hemos hablado nada del segundo EigenVector, formado por nzdusd=-2.4670 y chfusd=8.5409. Normalmente tomamos siempre el primer EigenVector ya que tiene un “tiempo de vida menor”: un HalfLife menor, es decir, la probabilidad de reversión a la media con los valores del primer EigenVector es mayor. Más tarde calcularemos el HalfLife de ambos spreads obtenidos por cada uno de los EigenVectors.

Una vez obtenidos los valores de los pesos de los componentes que forman nuestro portfolio, podemos ver cómo se comportan en un entorno pasado “in-sample”, irreal, ya que no podemos volver al pasado para probar. Posteriormente en la sección final veremos cómo lo aplicamos al periodo “out-sample”.
Calculamos el spread in-sample con los valores del primer eigenvector ” johansenGanador.evec(: , 1)” :
spreadInSample=[nzdusd chfusd]*johansenGanador.evec(:, 1);

Dibujamos el spread:

3

La verdad que no parece un spread muy atractivo. Probemos si es un proceso estacionario mediante el test de Dickey-Fuller

>> prt(adf(spreadInSample,0,1))

 

Augmented DF test for unit root variable:                   variable   1

ADF t-statistic       # of lags   AR(1) estimate

-3.447834               1         0.994458

 

1% Crit Value    5% Crit Value   10% Crit Value

-3.458           -2.871           -2.594

El test nos muestra que estamos aproximadamente al 1%, con lo cual una probabilidad cercana al 99% de pensar en un proceso estacionario.

Vamos a calcular el zscore. Este parámetro en sí mismo no posee magnitud, pero nos indicará el setup de entrada y salida de las operaciones en una estrategia simple.

zscore = ( spread -media(spread, n=lookback) ) / desviación_típica(spread,lookback)

movingAverageInSample = tsmovavg(spreadInSample,’s’,lookback,1);  %creamos la media móvil de 10 periodos

 

for t=lookback:size(spreadInSample, 1)   %recorremos el spread mediante rolling window igual a la lookback

stdInSample (t,:)=std(spreadInSample(t-lookback+1:t, :),1);  %desviación típica

end

zscoreInSample=(spreadInSample- movingAverageInSample)./ stdInSample;  %zscore para el setup

 

Para el cálculo de la desviación típica he seguido la segunda fórmula que Matlab nos proporciona para el cálculo de la desviación:

4

5

 

Vemos que es altamente estacionario. Las líneas verdes indican el 80% del rango máximo del zscore:

>> max(zscoreInSample)*0.8

 

ans =

 

2.2365

 

>> min(zscoreInSample)*0.8

 

ans =

 

-2.2972

 

Bien, esto no significa que operando el zscore tengamos reversiones constantemente, es decir, el entrar al 80% no garantiza una reversión instantánea, ya que para cada unidad de tiempo posterior (cada día posterior) se calcula una nueva muestra de zscore (spread- media)/std.

Supongamos que hemos alcanzado el +80% del rango y esperamos reversión hacia abajo. El zscore del día siguiente puede ser inferior al del día de ayer pero puede empezar a subir. Si esto ocurre, estaremos en pérdidas constantes hasta que no haya una regresión constante. Esto se ve claramente en ejemplos de trading para time frames pequeños del orden de 10, 15,30 minutos.

Podemos también calcular el tiempo de HalfLife que interpretaríamos como el plazo máximo de tiempo en el que la reversión se produzca. En varios estudios se utiliza el stop-loss de nuestras posiciones como el Halflife. Si pasado ese tiempo no se ha producido reversión a la media, cerraríamos posiciones. Esto puede o no puede ser óptimo siempre, la estrategia de salida de nuestras posiciones es algo que nosotros mismo como trader debemos de afrontar de acuerdo a nuestro sesgo.

Para calcular el Halflife podemos crear una función en Matlab con el siguiente código

%calcula el halflife de la serie en funcion del indice dado por Johansen y

%del spread calculado por Johansen

function halflife = HalfLife(spread)

shiftArray= circshift(spread,-1); %desplazamiento circular hacia la izquierda

shiftArray(end)=nan;

shiftArray = shiftArray-spread;

shiftArray(end)=[];

spread(end)=[];

 

regres=ols(shiftArray, spread-mean(spread)); %regresión mediante minimos cuadrados ordinarios.

halflife=-log(2)/regres.beta

end

 

>> HalfLife(spreadInSample)

 

halflife =

 

126.0066

 

Obtenemos un Halflife de 126 días, demasiado elevado quizás. Si en 126 días no se ha producido reversión a la media, cerraríamos posiciones.

 

Si utilizamos el segundo eigenvector anterior (-2.4670 y 8.5409)

>> johansenGanador.evec

ans =

19.4199   -2.4670                                                %19.4199 -> lotaje correspondiente al nzdusd

-9.7712    8.5409                           % -9.7712 ->  lotaje correspondiente al usdchf

calculamos el spread y el Halflife:

spreadInSample2=[nzdusd chfusd]*johansenGanador.evec(:, 2);

 

>> HalfLife(spreadInSample2)

halflife =

  473.3816

obtendríamos 473 días máximo esperando la reversión, por ello, suele utilizarse preferiblemente el primer eigenvector del test de Johansen.

 

Finalmente, nos queda calcular el backtest del periodo in-sample y out-sample, donde no usaremos gastos de transacciones. Para el cálculo del retorno podemos usar una función que nos devuelva el propio retorno estacionario. Finalmente calculamos la suma acumulada del mismo.

Para el backtest yo utilizo una función que he adaptado siguiendo procedimientos del maestro E.P.Chan.

function retorno = Backtest(zscore, umbral, pair1, pair2,johansenGanador) %umbral es el porcentaje del rango. Un 80% suele ser correcto

 

exitZscore=0; %salimos al retornar a 0 el zscore

longEntry=zscore < min(zscore)*umbral; %el rango del zscore depende de la lookback

shortEntry=zscore > max(zscore)*umbral;

longExit=zscore >= -exitZscore; %salimos cuando venimos del -80% y el zscore sube hasta 0

shortExit=zscore <= exitZscore; %salimos cuando venimos del +80% y el zscore baja hasta 0

 

positionsLong=NaN(size(zscore)); % long spread positions

positionsLong(longEntry)=1; %entrada en largo del spread

positionsLong(longExit)=0; %salimos del largo porque el zscore es mayor o igual que 0

 

%ponemos 1 o 0 según corresponda para las entradas en largo

for index=2:size(positionsLong, 1)

booleanNAN=~isfinite(positionsLong(index, :));

positionsLong(index, booleanNAN)=positionsLong(index-1, booleanNAN);

end

positionsLong(isnan(positionsLong))=0; %quitamos los NAN

 

positionsShort=NaN(size(zscore));

positionsShort(shortEntry)=-1; %entrada en corto del spread

positionsShort(shortExit)=0;    %salimos del corto porque el zscore es menor o igual que 0

 

%ponemos 1 o 0 según corresponda para las entradas en corto

for index=2:size(positionsShort, 1)

booleanNAN=~isfinite(positionsShort(index, :));

positionsShort(index, booleanNAN)=positionsShort(index-1, booleanNAN);

end

positionsShort(isnan(positionsShort))=0;

positionsSpread=positionsLong + positionsShort;

 

 

positions = zeros(size(positionsSpread, 1), 2);

positions(positionsSpread>0, 1)= pair1(positionsSpread>0).*johansenGanador.evec(1, 1);

positions(positionsSpread>0, 2)= pair2(positionsSpread>0).*johansenGanador.evec(2, 1);

 

positions(positionsSpread<0, 1)=-pair1(positionsSpread<0).*johansenGanador.evec(1, 1);

positions(positionsSpread<0, 2)=-pair2(positionsSpread<0).*johansenGanador.evec(2, 1);

 

 

%Beneficio y pérdida en dólares (USD)

shiftArrayPositions= circshift(positions,+1);

shiftArrayPositions(1)=nan;

shiftArrayPairs= circshift([pair1 pair2],+1);

shiftArrayPairs(1)=nan;

 

profitLoss = shiftArrayPositions.*([pair1 pair2]-shiftArrayPairs);

profitLoss(isnan(profitLoss))=0;

profitLoss=sum(shiftArrayPositions.*([pair1 pair2]-shiftArrayPairs), 2);

 

%Retornos

 

shiftArrayPositionsPairs = circshift(positions.*[pair1 pair2],+1);

shiftArrayPositionsPairs(1)=nan;

shiftArrayPositionsPairs = abs(shiftArrayPositionsPairs);

shiftArrayPositionsPairs(isnan(shiftArrayPositionsPairs))=0;

retorno=profitLoss./sum(shiftArrayPositionsPairs, 2);

 

end

 

%usamos un 80% del rango.

retornoInsample = Backtest(zscoreInSample,0.8,nzdusd,chfusd,johansenGanador);

 

 

Dibujamos la suma acumulada del retorno:

>> plot(cumsum(retornoInsample)); hold on; legend(‘Retorno In-Sample NZDUSD – USDCHF’);


 6

En 2 años, un 5% no es muy significativo, sumado que no hemos incluido los gastos por comisiones. Si cambiamos la lookback por un valor más ajustado obtendremos más número de operaciones y habitualmente un mayor drowdown.

 

Modificando el umbral al 60% obtendríamos:

>> retornoInSample = Backtest(zscoreInSample,0.6,nzdusd,chfusd,johansenGanador);

>> plot(cumsum(retornoInSample)); hold on; legend(‘Retorno In-Sample NZDUSD – USDCHF’);

 7

Obtendríamos una rentabilidad algo mayor al 10% y un drow cercano al 10% también. Hay que tener también en cuenta que al disminuir el umbral, el número de operaciones también aumenta y con ello los gastos de transacción.

 

Finalmente vamos a realizar un backtest para un periodo out-sample desde el 1 de enero de 2015 hasta el 23 de julio de 2015. Para ello, nos descargamos la data o la obtenemos de alguna forma.

 

> myTableOutSample = getDataFromDukasv2(operacion,instrumentos,timeFrameStr,’01-01-2015′,’23-07-2015′);

 

8

 

Ajustamos la data referenciando ambas divisas al dólar americano:

 

nzdusd = myTableOutSample(:,3);

nzdusd = table2array(nzdusd); %convierte a array

usdchf = myTableOutSample (:,4);

usdchf = table2array(usdchf); %convierte a array

chfusd =1./ usdchf;

 

Calculamos el spread:

 

>> spreadOutSample=[nzdusd chfusd]*johansenGanador.evec(:, 1);

 

Pasamos un test para comprobar cómo de estacionario es el spread:

>> prt(adf(spreadOutSample,0,1))

 

Augmented DF test for unit root variable:                   variable   1

ADF t-statistic       # of lags   AR(1) estimate

-1.409152               1         0.977781

 

1% Crit Value    5% Crit Value   10% Crit Value

-3.493           -2.876           -2.569

 

Viendo únicamente el resultado del test ya nos podemos dar una idea de que la gráfica del spread no será nada estacionaria:

 

>> plot(spreadOutSample)

 9

Tristemente con estos valores, nuestro retorno será muy caótico. Vamos a calcular nuestro zscore:

 

movingAverageOutSample = tsmovavg(spreadOutSample,’s’,lookback,1);             %creamos la media móvil de 10 periodos

 

for t=lookback:size(spreadOutSample, 1)   %recorremos el spread mediante rolling window igual a la lookback

stdOutSample (t,:)=std(spreadOutSample(t-lookback+1:t, :),1);  %desviación típica

end

zscoreOutSample=(spreadOutSample- movingAverageOutSample)./ stdOutSample;  %zscore para el setup

 

plot(zscoreOutSample);

legend(‘Zscore out-sample NZDUSD,CHFUSD’);

hold on;

plot(get(gca,’xlim’), [max(zscoreOutSample)*0.8 max(zscoreOutSample)*0.8],’green’);

plot(get(gca,’xlim’), [min(zscoreOutSample)*0.8 min(zscoreOutSample)*0.8],’green’);

hold off;

10

 

 

Finalmente calculamos el backtest out sample:

retornoOutSample = Backtest(zscoreOutSample,0.8,nzdusd,chfusd,johansenGanador);

plot(cumsum(retornoOutSample)); hold on; legend(‘Retorno Out-Sample NZDUSD – USDCHF’);hold off;

11

En estos 7 meses de 2015 hubiéramos perdido un 2% en caso de seguir con la misma estrategia de lookback 10 y umbral para el zscore del 80%

Con un umbral del 60% por ahora sería un poco menos malo, pero nada alentador como estrategia.

retornoOutSample = Backtest(zscoreOutSample,0.6,nzdusd,chfusd,johansenGanador);

plot(cumsum(retornoOutSample)); hold on; legend(‘Retorno Out-Sample NZDUSD – USDCHF’);hold off;

12

Con todo esto, podemos comprobar cómo afecta el periodo in-sample y out-sample en nuestra estrategia. Probar los parámetros de Johansen en el pasado es bastante diferente a usarlos en un periodo posterior, y por consiguiente sus resultados.

 

Faltaría por optimizar muchas más cosas, como:

1) Buscar parámetros de cointegración más robustos en periodos in-sample.

2) Modificar la lookback y probar diferentes resultados.

3) Cambiar el time-frame y probar en datos intradiarios, aunque si bien es cierto, la cointegración es algo que suele aplicarse en periodos largos de tiempo.