Continuamos con la segunda parte de este artículo exponiendo la implementación el la plataforma NinjaTrader de nuestro método para la creación dinámica de series sintéticas y la evaluación automatizada de estrategias en dichas series.
4.- Punto de partida: Series calculadas en Excel barra a
barra
Inicialmente, se parte de los cálculos barra a barra que alteran la serie original en sus modalidades con y sin anclaje:
Los cálculos han de hacerse barra
a barra en base a tres números aleatorios que regulan las alteraciones del
máximo, mínimo y cierre, así como el promedio de volatilidad de barras
anteriores.
Con estos cálculos bien definidos
y vista su viabilidad grosso-modo en el Excel, eso nos da la estructura general
del algoritmo barra a barra, y ya nos podemos lanzar a implementarlo en la
plataforma concreta.
A la hora de implementar este
proceso en una plataforma de trading, lo que se persigue es poder hacer
backtesting de estrategias sobre series OHLC alteradas, es decir, llevar a cabo
un proceso similar al de optimización (en el que se realizan un número de
iteraciones de backtesting en un período de tiempo dado), pero en lugar de
alterar los parámetros de la estrategia, alteraremos las series OHLC sobre las
que se ejecutan, analizando a posteriori los resultados del backtesting a nivel
de trade y de resultados globales.
Así, conceptualmente, la implementación debe cumplir estas características:
Se ha elegido implementarlo en NinjaTrader 7 por compatibilidad con el código de estrategias existentes, familiaridad con el código fuente y el desarrollo de módulos y su depuración, y la integración con Excel. El desarrollo en NinjaTrader versión 8 sería muy parecido y según se estudió durante el proceso, no aporta ventajas al respecto.
Inicialmente se da por bueno el
optimizador general que lleva NinjaTrader y bastaría simplemente con añadir un
parámetro “iteracion” a la estrategia que no se utiliza en ella, sino que vaya
toma valores desde 1 a N siendo N el número de iteraciones que se persiguen.
El primer paso lógico es crear un
mecanismo que genere la serie alterada barra a barra y que en cada iteración la
sustituya por la original. En cada barra de la ejecución de una estrategia en
NinjaTrader, cuyo código se ubica en el procedimiento OnBarUpdate,
existen cuatro DataSeries que nos permiten acceder a las series OHLC,
llamados Open, High, Low y Close.
Por ejemplo, supongamos un ejemplo de estrategia extremadamente simple que abre posición si el cierre de la barra es mayor que la EMA de 100 períodos y la cierra si cae por debajo:
protected override
void OnBarUpdate()
{
if (Close[0] >
EMA(Close, 100) && Position.MarketPosition != MarketPosition.Long)
EnterLong(1,"");
if (Close[0] <
EMA(Close, 100) && Position.MarketPosition == MarketPosition.Long)
ExitLong("","");
}
Así pues, la situación ideal
sería no cambiar absolutamente nada del código de la estrategia y justo antes
del comienzo de cada iteración generar los 4 DataSeries alterados que
serían accesibles bajo la misma denominación. Lamentablemente, y tras consultar
al servicio técnico de NinjaTrader, esto no es posible dado que la generación
de estas series de datos forma parte del núcleo de código fuente y no se pueden
alterar los DataSeries Open, High, Low y Close en ninguna de sus
versiones, siendo siempre de sólo lectura.
Para resolver este problema, la
opción más evidente es crear un indicador (llamado por ejemplo OHLCDespl)
que barra a barra calcule las series alteradas de cada iteración y las contenga,
siendo referenciado en el código de la estrategia. En realidad, en todo caso
había que hacer un indicador para la representación gráfica de las series
alteradas, pero tendrá la responsabilidad adicional de calcular esas series
barra a barra.
La principal desventaja de este enfoque es clara: la estrategia deberá modificarse para incluir referencias a este indicador en su código, sustituyendo a las referencias a los DataSeries Open, High, Low y Close, quedando así nuestro código de ejemplo:
protected override
void OnBarUpdate()
{
if ( OHLCDespl().Close[0] > EMA(OHLCDespl().Close,
100) &&
Position.MarketPosition !=
MarketPosition.Long )
EnterLong(1,"");
if (
OHLCDespl().Close[0] < EMA(OHLCDespl().Close, 100) &&
Position.MarketPosition ==
MarketPosition.Long)
ExitLong("","");
}
En la práctica el indicador OHLCDespl()
tendrá parámetros como el número de barras para el promedio de volatilidad
o el número de días para reiniciar el anclaje (en series con anclaje) y algún
otro para agilizar el proceso de cálculo.
El cálculo del indicador es relativamente sencillo. Se utilizarán en su interior tantos DataSeries como columnas haya en el Excel de definición de los cálculos. Ello facilita enormemente su desarrollo y depuración en caso de problemas. Por ejemplo para calcular los ticks al alza, a la baja y entre cierres, se definen tres DataSeries UpTicks, DownTicks y CloseTicks que los van calculando en base a la serie original y que sirven como base para cálculos posteriores:
UpTicks[0]=(High[0]-Open[0])/Instrument.MasterInstrument.TickSize;
DownTicks[0]=(Open[0]-Low[0])/Instrument.MasterInstrument.TickSize;
CloseTicks[0]=
Math.Abs(Close[0]-Close[1])/Instrument.MasterInstrument.TickSize;
El punto crítico del cálculo de las series alteradas es cuando se definen los valores de los DataSeries HighA, LowA y CloseA que determinan unos ticks arriba o abajo al azar en base a otros valores calculados de volatilidad. En definitiva es el único sitio del cálculo donde se deben usar valores aleatorios, por lo que es crítico para la variabilidad de los resultados. El método más razonable que se ha encontrado para que esa aletoriedad fuera representativa ha sido usar como semilla un hashing del Guid de .NET:
var rand = new
Random(Guid.NewGuid().GetHashCode());
var ticksize=
Instrument.MasterInstrument.TickSize;
HighA[0]=
rand.Next(-1* (int) UpVolat[0], (int) UpVolat[0])* tickSize;
LowA[0]=
rand.Next(-1* (int) DownVolat[0], (int) DownVolat[0])*tickSize;
CloseA[0]=
rand.Next(-1* (int) CloseVol[0], (int) CloseTicks[0])*tickSize;
Así la serie alterada oscilará
aleatoriamente entre los valores de volatilidad calculados de una forma
bastante razonable.
Otro aspecto fundamental del indicador es que los valores de serie alterada se representasen de una forma visual para su revisión.
En la representación gráfica del
indicador se superpone una barra roja o verde con transparencia mostrando la
alteración en la misma, así como la línea de mínimos y máximos con un poco más
de grosor.
Para conseguir esta representación gráfica hay que definir unos elementos Brush (con transparencia) y Pen en el procedimiento OnStartUp y luego utilizarlos sobreescribiendo el procedimiento Plot que se encarga de dibujar (código simplificado valX es la serie alterada):
protected override void OnStartUp()
{
if (ChartControl == null || Bars ==
null)
return;
brocharoja = new
SolidBrush(Color.FromArgb(50,Color.DarkRed));
brochaverde = new
SolidBrush(Color.FromArgb(50,Color.DarkGreen));
lapiz = new Pen(Color.Black, 2);
//linea high low
si se quiere con guiones
//lapiz.DashStyle=DashStyle.Dash;
}
public override void
Plot(Graphics graphics, Rectangle bounds, double min, double max)
{
int ancho = (int)
(Math.Max(3, 1 + 2 * ((int)Bars.BarsData.ChartStyle.BarWidth - 1) + 2)*1.5);
for (int i =
FirstBarIndexPainted; i <= LastBarIndexPainted; i++)
{
if (i - Displacement < 0 ||
i - Displacement >= BarsArray[0].Count ||
(!ChartControl.ShowBarsRequired
&& i - Displacement < BarsRequired))
continue;
int x =
ChartControl.GetXByBarIdx(BarsArray[0], i);
int y1 =
ChartControl.GetYByValue(this, valO);
int y2 =
ChartControl.GetYByValue(this, valH);
int y3 =
ChartControl.GetYByValue(this, valL);
int y4 =
ChartControl.GetYByValue(this, valC);
//desplazamiento lateral
lineas high low. con 1 pixel se solapa un poco
int despl_x_hl=1;
//linea high
graphics.DrawLine(lapiz,
x+despl_x_hl, Math.Min(y1,y4), x+despl_x_hl, y2);
//linea low
graphics.DrawLine(lapiz,
x+despl_x_hl, Math.Max(y1,y4), x+despl_x_hl, y3);
if (y4 == y1)
graphics.DrawLine(lapiz, x - ancho / 2, y1, x + ancho / 2, y1);
else
{ //barra en verde o rojo
if (y4 >
y1)
graphics.FillRectangle(brocharoja, x - ancho / 2, y1, ancho, y4 - y1);
else
graphics.FillRectangle(brochaverde, x - ancho
/ 2, y4, ancho, y1 - y4);
}
} //for
}
En este punto ya tenemos un
indicador que calcula las series alteradas y las representa gráficamente y al
que podemos referenciar desde las estrategias en lugar de las series
originales. El siguiente problema a resolver es que no se puede sobreescribir o
alterar los métodos que abren posiciones tipo EnterLong o EnterShort para que esas posiciones se materialicen
fuera de la serie original, con lo que aunque se calculen bien los setups y se
usen las series alteradas, las posiciones sólo se podrán materializar si
estamos dentro del los precios de la serie original.
La solución pasa por modificar un
aspecto que sí permite NinjaTrader y que es el algoritmo de Fill, regulador del
precio al que se abrirán las órdenes en sus distintas modalidades: a mercado,
limitadas, de stop…
NinjaTrader proporciona dos tipos de Fill: Default o Liberal, cuyo código está disponible. Centrándonos por simplicidad en órdenes a mercado, el código para ambas es tan sencillo como que se abre posición en el precio de la apertura siguiente barra de la serie original:
public override void Fill(Order
order)
{
if
(order.OrderType == OrderType.Market)
{
FillPrice=NextOpen;
}
….
// resto de tipos de orden
}
Como se puede ver, lo único que hay que hacer es
implementar un tipo Fill personalizado en el que el precio de apertura sea no
el NextOpen de la serie original sino el NextOpen de la serie
alterada. El problema es que la serie
alterada reside únicamente en el indicador OHLCDespl que hemos creado, que se
instancia y referencia dentro del código de la estrategia, por lo que esos
valores alterados son inaccesibles desde el código de un tipo Fill
personalizado.
La solución consiste en definir una clase estática en C# que contenga código personalizado, el cual nos hará de intermediario entre el indicador, la estrategia y cualquier otro elemento que nos interese (en este caso el tipo de Fill personalizado).
Estas clases son muy útiles cuando hay mucho código
que conviene recaiga fuera del indicador o estrategia. Aunque en este caso es
para poca cosa, luego iremos metiendo en ella más código auxiliar para
controlar iteraciones o la exportación a Excel. Llamaremos a la clase estática
OHLCDesplAux, metemos el código en un simple fichero OHLCDesplAux.cs en la
carpeta de código y de momento será tan simple como una variable para el precio
con su getter y setter:
namespace
NinjaTrader.Indicator
{
public static class OHLCDesplAux
{
private static
double oHLCDesplNextOpen;
public static
double OHLCDesplNextOpen {
get{
return oHLCDesplNextOpen; }
set{
if(value
!= oHLCDesplNextOpen){
oHLCDesplNextOpen
= value;
}
}
}
}
Ahora que ya tenemos un intermediario visible desde
cualquier estrategia o indicador en ejecución y desde el tipo de Fill, sólo
queda que el indicador escriba el valor:
…
double SiguienteOpen=0;
switch(tipoSerie){
case
0: //con anclaje
SiguienteOpen=CloseCAaux[0];
break;
case
1: //sin anclaje
SiguienteOpen=CloseSAaux[0];
break;
}
//Valor
del siguiente Open para el algoritmo de Fill
OHLCDesplAux.OHLCDesplNextOpen=SiguienteOpen;
…
y que el tipo de Fill personalizado lo consuma:
[Gui.Design.DisplayName("OHLCDespl")]
public class OHLCDesplFillType :
FillType
{
private const double epsilon = 0.00000001;
public override
void Fill(Order order)
{
if
(order.OrderType == OrderType.Market)
{
FillPrice=OHLCDesplAux.OHLCDesplNextOpen;
//FillPrice=NextOpen;
}
….
Una vez puestas las referencias, NinjaTrader compilará el código de la clase personalizada como un fichero más. Así, ya tenemos un tipo de Fill personalizado, accesible desde la GUI, que permitirá que las posiciones se materialicen en los precios de la serie alterada:
Una vez implementado todo y funcionando adecuadamente, nos encontramos con un nuevo contratiempo. En NinjaTrader, cuando se le da a examinar los resultados de las iteraciones y su graficación se queda un tiempo procesando información antes de mostrarla:
Al no estar completamente documentados todos los
comportamientos de la plataforma, podría pensarse que ese lapso de tiempo es
debido a que está recopilando y representando la información contenida en memoria
acerca del proceso de optimización, pero qué va: en realidad en el preciso momento
en el que se selecciona una iteración en el listado de resultados de arriba,
NinjaTrader inicia una instancia de la estrategia con los parámetros dados de
la iteración y tras procesarla, grafica y muestra los resultados. Este
comportamiento subóptimo (la información está en memoria y podría acceder a
ella sin pegas) se sigue reproduciendo en NinjaTrader 8.
Esta clara limitación de NinjaTrader plantea un
problema: los números aleatorios generados en cada barra de cada serie de cada
iteración se vuelven a refrescar (por otros diferentes) cada vez que se
selecciona una estrategia, por lo que será imposible ver la graficación y
resultados correspondientes a una iteración dada, que se irá ejecutando sobre
series alteradas diferentes. Esto es un grave problema que echa por tierra la
posibilidad de examinar y graficar una iteración concreta, siempre se alteraría
por otra nueva.
La solución a este problema pasa de nuevo por la clase
estática en C#. En ella se definará una matriz de números aleatorios, tres por
cada barra de cada iteración, de manera que siempre que se vuelva a una
iteración dada, ésta siempre se ejecutará usando los mismos números aleatorios
(y por lo tanto la misma serie alterada) y sus resultados y graficación serán
los mismos.
private
static List> aleatorios1 = new
List
>();
private
static List> aleatorios2 = new
List
>();
private
static List> aleatorios3 = new
List
>();
private
static int iteracion;
private
static int barras;
public
static int Barras {
get{ return barras; }
set{barras=value;}
}
public
static int Iteracion {
get{ return iteracion; }
set{
if(iteracion!=value)
llenar_aleatorios();
iteracion=value;
}
}
public
static int aleatorio_entero(int min, int max, int barra, int iteracion, int
indice){
double rnd=0;
switch(indice){
case 1:
rnd=aleatorios1[iteracion-1][barra];
break;
case 2: rnd=aleatorios2[iteracion-1][barra]; break;
case
3: rnd=aleatorios3[iteracion-1][barra];
break;
}
return (int) (rnd*(max-min)+min);
}
public
static void llenar_aleatorios(){
var rand = new
Random(Guid.NewGuid().GetHashCode());
List
List
List
for(int i=0;i
iteracionactual1.Add(rand.NextDouble());
iteracionactual2.Add(rand.NextDouble());
iteracionactual3.Add(rand.NextDouble());
}
aleatorios1.Add(iteracionactual1);
aleatorios2.Add(iteracionactual2);
aleatorios3.Add(iteracionactual3);
}
Como se puede ver en el código se definen tres matrices aleatorios1, aleatorios 2 y aleatorios3 que contienen los números necesarios para cada barra de cada iteración. Se llenan cuando se cambia de número de iteración (especificado por el setter de la variable) y para obtenerlos se usa la función aleatorio_entero cuya invocación desde el indicador sería ahora así:
var
tickSize= Instrument.MasterInstrument.TickSize;
HighA[0]=
OHLCDesplAux.aleatorio_entero(-1*UpVolat[0], UpVolat[0],CurrentBar,iter,2)
*tickSize;
LowA[0]=OHLCDesplAux.aleatorio_entero(1*DownVolat[0],DownVolat[0],CurrentBar,iter,2)
*tickSize;
CloseA[0]=OHLCDesplAux.aleatorio_entero(-1*CloseVol[0],CloseTicks[0],CurrentBar,iter,2)
*tickSize;
De esta manera cada iteración tendrá una serie
alterada reproducible y podrá analizarse y graficarse correctamente en la
pantalla de resultados.
Abundando en la pantalla de resultados, también se
observó que las estadísticas que allí aparecían no tenían ni pies ni cabeza
respecto a la ejecución de cada iteración, sus trades y graficación (en las
capturas ya está corregido pero en su momento salían valores imposibles). Tras
mucha investigación, se llegó a la conclusión de que había un problema de
concurrencia, generado por la ejecución multihilo de NinjaTrader que aunque
acelera el proceso, necesita de una sincronización fuerte entre procesos y
control de la concurrencia, que al parecer no se implementa adecuadamente si
hay dependencias entre ejecuciones.
Por ese motivo y para controlar la ejecución de cada iteración, llenado y limpieza de aleatorios y para exportación de resultados que se verá más adelante, se consideró necesario implementar una variante del optimizador de NinjaTrader, partiendo del estándar. Es un código sencillo que permite mantener el control y anular los problemas de concurrencia (sólo se exponen algunas funciones relevantes):
[Gui.Design.DisplayName("OHLCDespl")]
public
class OHLCDesplOptimizationMethod : OptimizationMethod
{
…
public override void
Initialize()
{
base.Initialize();
OHLCDesplAux.limpiar_aleatorios();
}
public override
bool MultiThreadSupport
{
get { return
false; }
}
public override void
Optimize()
{
//limpiamos
los numeros aleatorios de otras optimizaciones
Iterate(0);
WaitForIterationsCompleted(true);
OHLCDesplAux.iniciar_libro(4);
OHLCDesplAux.exportar_trades();
}
…
}
En el código del optimizador personalizado se puede ver cómo se anula el procesamiento multihilo, se limpian los aleatorios de otras ejecuciones y, aunque se verá más tarde, se espera a que todas las ejecuciones hayan terminado para exportar los resultados a Excel. Este optimizador personalizado deberá elegirse en la GUI de NinjaTrader igual que se hacía con el tipo de Fill:
Con todo ello tendremos un sólido y consistente
control sobre el proceso global que garantiza que se podrán analizar y
representar correctamente las iteraciones. Lamentablemente se pierder
rendimiento al anular el procesamiento multihilo, pero es ineludible dado que
no tenemos más control sobre el proceso seguido en NinjaTrader.
La pantalla de resultados ofrecida por NinjaTrader
es bastante limitada y sobre todo efímera, es accesible sólo hasta que se
cierra la ventana. La opción habitual para su exportación sería navegar por las
iteraciones una a una, en su pestaña de “Trades” y exportarlas individualmente
a un libro de excel. Está claro que ese proceso debe mejorarse y automatizarse
para que poder hacer un análisis viable.
La primera aproximación que se nos puede ocurrir
sería exportar los trades de cada iteración a un fichero de texto csv, por
ejemplo sobreescribiendo el método OnTermination de la estrategia (o
desde el optimizador personalizado si se dispone de él) expuesto de forma muy
simplificada:
protected
override void OnTermination()
{
StreamWriter sw;
String id=iteracion.ToString();
String path =
@"c:windows emp"+id+".csv";
if (!File.Exists(path))
sw =
File.CreateText(path);
foreach (Trade t in
Performance.AllTrades)
{
String
salida=id;
//datos
trade
salida=salida+separador+t.TradeNumber.ToString();
salida=salida+((t.Entry.MarketPosition==MarketPosition.Long)?separador+
"Long":separador+"Short");
salida=salida+t.Entry.Time.ToString(separador+"dd/MM/yyyy;HH:mm:ss");
salida=salida+t.Exit.Time.ToString(separador+"dd/MM/yyyy;HH:mm:ss");
salida=salida+separador+t.Exit.Price.ToString();
salida=salida+separador+Math.Round(t.ProfitCurrency).ToString();
sw.WriteLine(salida);
}
sw.Close();
}
Este sencillo código en cualquier estrategia en
NinjaTrader (que disponga de un parámetro “iteracion” que distinga cada
ejecución en el optimizador) permitirá, de forma general, exportar la
información de los trades de la misma con detalle de entrada, salida, precios y
profit and loss.
No obstante, para hacer un buen análisis de los
resultados sobre series alteradas, se requeriría posteriormente exportar toda
esta información a otro libro excel, calcular la series de equity y graficarlas
y otros estudios estadísticos. Este trabajo adicional puede ser muy tedioso y
comprometer la viabilidad de llevar a cabo estos estudios.
Por ese motivo, aprovechando que NinjaTrader
trabaja en C#, se ha optado por usar las capacidades de este lenguaje para la
automatización en Excel, lo que se denomina Microsoft Office Interop Excel
Automation.
Básicamente requiere que el equipo donde se ejecute NinjaTrader tenga también el Excel instalado, copiar al directorio bin y referenciar desde las opciones de References la DLL de Microsoft Office Interop:
(A esta pantalla se accede desde el editor de código de NinjaTrader, botón derecho y References)
y nombrarlas desde el código como la directiva using:
using
System.Runtime.InteropServices;
using
Excel = Microsoft.Office.Interop.Excel;
Aprovechando que tenemos una clase estática y tal y
como se ha visto en el código del optimizador personalizado en el apartado
anterior, todo el código de exportación se ha incluido allí por limpieza. A
continuación se muestra un código genérico que permite crear un libro de excel
con un número de hojas dado:
public
static void iniciar_libro(int hojas){
try
{
excelApp=
(Excel.Application)System.Runtime.InteropServices.Marshal.GetActiveObject("Excel.Application");
}
catch
{
excelApp = new
Microsoft.Office.Interop.Excel.Application();
}
excelWorkBook=excelApp.Workbooks.Add(Type.Missing);
int
i;
int
cuenta=excelWorkBook.Worksheets.Count;
Excel.Worksheet
xlWorkSheet;
if(cuenta<=hojas)
for(i=0;i
xlWorkSheet=(Excel.Worksheet)
excelWorkBook.Worksheets.Add((Excel.Worksheet)excelWorkBook.Worksheets.get_Item(1),System.Reflection.Missing.Value,hojas-cuenta,Excel.XlSheetType.xlWorksheet);
else
for(i=0;i
xlWorkSheet =
(Excel.Worksheet)excelWorkBook.Worksheets.get_Item(1);
xlWorkSheet.Delete();
}
/*
En este punto se introduciría todo el código de
Excel para llenar las hojas de valores, fórmulas, gráficos, etc, etc,
articulado en funciones
*/
String
path = @"c:windows empsalida.xlsx";
if
(File.Exists(path))
File.Delete(path);
excelWorkBook.SaveAs(path,Excel.XlFileFormat.xlOpenXMLWorkbook,Type.Missing,
Type.Missing, Type.Missing, Type.Missing, Excel.XlSaveAsAccessMode.xlExclusive,
Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing);
excelApp.Visible
= true;
excelApp.Quit();
Marshal.ReleaseComObject(excelWorkBook);
Marshal.ReleaseComObject(excelApp);
}
Como se puede ver, lo que hace la función es
invocar a la aplicación de Excel y, si todo ha ido bien, añadir un libro.
Cuando Excel inicia un nuevo libro suele tener 3 hojas, pero no es un parámetro
fijo, por lo que el código cuenta las hojas que hay y añade o elimina hasta
obtener las deseadas.
En la parte intermedia, antes de guardar el libro,
deberemos implementar las distintas funciones que llenen de valores, fórmulas y
gráficos las hojas del libro. También se podría hacer a posteriori reabriendo
el libro existente. En todo caso finalmente se guarda y se sale del Excel.
El resultado final es un libro con todos los resultados exportados y su análisis exportados en diferentes pestañas:
@ TradingSys.org, 2019