He empezado el tutorial de ML en NT8 y sino fuera por tus instrucciones cls, ni la IA puede entender qué han querido hacer los de NT, qué ensalada madre mia... pues eso GRACIAS cls !!!
1) mejor que pasar los dll directo a :
C:\Users\<user>\Documents\NinjaTrader 8\bin\Custom
crear una carperta "ML" dentro de Custom y moverlos allí, así se evita que distintas versiones de NT8 entren en conflicto con los dll. A medida q avanzan las versiones de Ninja cambian los packages a los que se refieren, empiezan a ser distintos a los de los dll del tutorial, y comienzan los problemas...
Cargar los "references" luego desde ahí, el indicador usará sus dll de la carpeta ML, NT usará los propios de custom, y cada verdura en su cajón.
Además de los links de cls, agrego los que faltan:
C:\Users\<user>\.nuget\packages\newtonsoft.json\13.0.1\lib\net45
C:\Users\<user>\.nuget\packages\system.memory\4.5.5\lib\netstandard2.0
C:\Users\<user>\.nuget\packages\microsoft.ml.cpumath\3.0.0\lib\netstandard2.0
Totalizando 10 dll dentro de:
C:\Users\<user>\Documents\NinjaTrader 8\bin\Custom\ML
FastTreeNative.dll
Microsoft.ML.Core.dll
Microsoft.ML.CpuMath.dll
Microsoft.ML.Data.dll
Microsoft.ML.DataView.dll
Microsoft.ML.dll
Microsoft.ML.FastTree.dll
netstandard.dll
Newtonsoft.Json.dll
System.Memory.dll
y referenciando 6 dentro del código como lo muestra el tutorial:
Microsoft.ML.Core.dll
Microsoft.ML.Data.dll
Microsoft.ML.DataView.dll
Microsoft.ML.dll
Microsoft.ML.FastTree.dll
netstandard.dll
2) Curiosidades:
* el tutorial habla de ATR de 1 período, lo que en realidad es un True Range a secas.
** el uso de "float" en vez de "double" aquí chatGPT hace una buena advertencia, mejor usar doubles y no floats... me remito a la IA para dicha explicación... pero... supongo que Ninja aquí piensa en float porque ML está pensado a procesamiento en GPU y entonces sí float rinde mejor (?)
3) Reorganizada de código (iré actualizando)
Código: Seleccionar todo
#region Using declarations
using Microsoft.ML;
using Microsoft.ML.Data;
using Microsoft.ML.Trainers.FastTree;
using NinjaTrader.Gui;
using NinjaTrader.NinjaScript.DrawingTools;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Windows.Media;
#endregion
namespace NinjaTrader.NinjaScript.Indicators.MLNET
{
public class MLTrendPrediction : Indicator
{
private MLContext mlContext;
private TransformerChain<RegressionPredictionTransformer<FastTreeRegressionModelParameters>> closeModel;
protected override void OnStateChange()
{
#region SetDefaults
if (State == State.SetDefaults)
{
Description = @"Predicting Price Trends Using ML.NET in NinjaTrader 8";
Name = "MLPrediction";
Calculate = Calculate.OnBarClose;
IsOverlay = true;
DisplayInDataBox = true;
DrawOnPricePanel = true;
DrawHorizontalGridLines = true;
DrawVerticalGridLines = true;
PaintPriceMarkers = true;
ScaleJustification = Gui.Chart.ScaleJustification.Right;
//Disable this property if your indicator requires custom values that cumulate with each new market data event.
//See Help Guide for additional information.
IsSuspendedWhileInactive = true;
TrainingPeriod = 500;
LookbackPeriod = 12;
PredictionHorizon = 14;
PredictionLoop = 300;
NumberOfLeaves = 20;
MinimumExampleCounterPerLeaf = 20;
LearningRate = 0.05;
}
#endregion
else if (State == State.DataLoaded)
{
mlContext = new MLContext();
}
}
#region HELPERS
#region ML DLLs Loader
static MLTrendPrediction()
{
AppDomain.CurrentDomain.AssemblyResolve += ResolveML;
}
private static System.Reflection.Assembly ResolveML(object sender, ResolveEventArgs args)
{
string basePath = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
@"NinjaTrader 8\bin\Custom\ML");
string dllName = new System.Reflection.AssemblyName(args.Name).Name + ".dll";
string fullPath = System.IO.Path.Combine(basePath, dllName);
if (System.IO.File.Exists(fullPath))
return System.Reflection.Assembly.LoadFrom(fullPath);
return null;
}
#endregion
#region BarData class
public class BarData
{
public float Open1 { get; set; }
public float Open2 { get; set; }
public float Open3 { get; set; }
public float Open4 { get; set; }
public float High1 { get; set; }
public float High2 { get; set; }
public float High3 { get; set; }
public float High4 { get; set; }
public float Low1 { get; set; }
public float Low2 { get; set; }
public float Low3 { get; set; }
public float Low4 { get; set; }
public float Close1 { get; set; }
public float Close2 { get; set; }
public float Close3 { get; set; }
public float Close4 { get; set; }
public float ATR1 { get; set; }
public float ATR2 { get; set; }
public float ATR3 { get; set; }
public float ATR4 { get; set; }
public float EMA1 { get; set; }
public float EMA2 { get; set; }
public float EMA3 { get; set; }
public float EMA4 { get; set; }
public float FutureClose { get; set; }
}
public class ClosePrediction
{
[ColumnName("Score")]
public float FutureClose { get; set; }
}
#endregion
#region CalculateIndices
private List<int> CalculateIndices(int period)
{
var indices = new List<int>();
double step = period / 3.0;
for(int i = 0; i <= 3; i++)
{
indices.Add((int)Math.Round(i * step));
}
return indices;
}
#endregion
#region PrepareData
public List<BarData> PrepareData()
{
List<BarData> data = new List<BarData>();
List<int> idx = CalculateIndices(LookbackPeriod);
for(int i = 1; i < TrainingPeriod; i++)
{
data.Add(new BarData
{
Open1 = (float)Open[i + PredictionHorizon],
Open2 = (float)Open[i + PredictionHorizon + idx[1]],
Open3 = (float)Open[i + PredictionHorizon + idx[2]],
Open4 = (float)Open[i + PredictionHorizon + idx[3]],
High1 = (float)High[i + PredictionHorizon],
High2 = (float)High[i + PredictionHorizon + idx[1]],
High3 = (float)High[i + PredictionHorizon + idx[2]],
High4 = (float)High[i + PredictionHorizon + idx[3]],
Low1 = (float)Low[i + PredictionHorizon],
Low2 = (float)Low[i + PredictionHorizon + idx[1]],
Low3 = (float)Low[i + PredictionHorizon + idx[2]],
Low4 = (float)Low[i + PredictionHorizon + idx[3]],
Close1 = (float)Close[i + PredictionHorizon],
Close2 = (float)Close[i + PredictionHorizon + idx[1]],
Close3 = (float)Close[i + PredictionHorizon + idx[2]],
Close4 = (float)Close[i + PredictionHorizon + idx[3]],
ATR1 = (float)ATR(1)[i + PredictionHorizon],
ATR2 = (float)ATR(1)[i + PredictionHorizon + idx[1]],
ATR3 = (float)ATR(1)[i + PredictionHorizon + idx[2]],
ATR4 = (float)ATR(1)[i + PredictionHorizon + idx[3]],
EMA1 = (float)EMA(LookbackPeriod)[i + PredictionHorizon],
EMA2 = (float)EMA(LookbackPeriod)[i + PredictionHorizon + idx[1]],
EMA3 = (float)EMA(LookbackPeriod)[i + PredictionHorizon + idx[2]],
EMA4 = (float)EMA(LookbackPeriod)[i + PredictionHorizon + idx[3]],
FutureClose = (float)Close[i]
});
}
return data;
}
#endregion
#region TrainModel
private void TrainModel()
{
var data = PrepareData();
if(!data.Any())
{
Print("Error: no hay datos disponibles para el entrenamiento");
return;
}
var trainingDataView = mlContext.Data.LoadFromEnumerable(data);
try
{
var pipelineClose =
mlContext.Transforms.Concatenate("Features",
"Open1", "Open2", "Open3", "Open4",
"High1", "High2", "High3", "High4",
"Low1", "Low2", "Low3", "Low4",
"Close1", "Close2", "Close3", "Close4",
"ATR1", "ATR2", "ATR3", "ATR4",
"EMA1", "EMA2", "EMA3", "EMA4")
.Append(mlContext.Transforms.CopyColumns(outputColumnName: "Label", inputColumnName: "FutureClose"))
.Append(mlContext.Regression.Trainers.FastTree(
numberOfLeaves: NumberOfLeaves,
minimumExampleCountPerLeaf: MinimumExampleCounterPerLeaf,
learningRate: LearningRate));
closeModel = pipelineClose.Fit(trainingDataView);
var metricsClose = mlContext.Regression.Evaluate(closeModel.Transform(trainingDataView));
Print("RSquared: " + metricsClose.RSquared + " RMSE: " + metricsClose.RootMeanSquaredError);
Print("TrainModel completado con éxito");
}
catch (Exception ex)
{
Print("Error durante el TrainModel: " + ex.ToString());
}
}
#endregion
#region PredictLevels
private void PredictLevels()
{
if(closeModel == null)
{
Print("Error: modelo no entrenado... no hay predicción");
return;
}
try
{
var predictionEngineClose = mlContext.Model.CreatePredictionEngine<BarData, ClosePrediction>(closeModel);
List<int> idx = CalculateIndices(LookbackPeriod);
var currentbarData = new BarData
{
Open1 = (float)Open[0],
Open2 = (float)Open[idx[1]],
Open3 = (float)Open[idx[2]],
Open4 = (float)Open[idx[3]],
High1 = (float)High[0],
High2 = (float)High[idx[1]],
High3 = (float)High[idx[2]],
High4 = (float)High[idx[3]],
Low1 = (float)Low[0],
Low2 = (float)Low[idx[1]],
Low3 = (float)Low[idx[2]],
Low4 = (float)Low[idx[3]],
Close1 = (float)Close[0],
Close2 = (float)Close[idx[1]],
Close3 = (float)Close[idx[2]],
Close4 = (float)Close[idx[3]],
ATR1 = (float)ATR(1)[0],
ATR2 = (float)ATR(1)[idx[1]],
ATR3 = (float)ATR(1)[idx[2]],
ATR4 = (float)ATR(1)[idx[3]],
EMA1 = (float)EMA(LookbackPeriod)[0],
EMA2 = (float)EMA(LookbackPeriod)[idx[1]],
EMA3 = (float)EMA(LookbackPeriod)[idx[2]],
EMA4 = (float)EMA(LookbackPeriod)[idx[3]],
};
var predictionClose = predictionEngineClose.Predict(currentbarData);
Print("Predicción = " + predictionClose.FutureClose);
Draw.ArrowLine(this, "PredictDirection " + CurrentBar + " " + PredictionHorizon, 0, Close[0],
-PredictionHorizon, predictionClose.FutureClose,
Close[0] < predictionClose.FutureClose ? Brushes.DodgerBlue :
Close[0] > predictionClose.FutureClose ? Brushes.Magenta : Brushes.Silver,
DashStyleHelper.Solid, 3);
}
catch (Exception ex)
{
Print("Error durante la predicción: " + ex.ToString());
}
}
#endregion
#endregion
protected override void OnBarUpdate()
{
if (BarsInProgress != 0 || CurrentBar < Math.Max(BarsRequiredToPlot, PredictionHorizon + TrainingPeriod))
return;
try
{
if (CurrentBar % PredictionLoop == 0)
{
TrainModel();
PredictLevels();
}
}
catch (Exception ex)
{
Print("Error en OnBarUpdate: " + ex.ToString());
}
}
#region Properties
[Range(100, int.MaxValue), NinjaScriptProperty]
[Display(ResourceType = typeof(Custom.Resource), Name = "Training Period", Description = "Número de barras para el entrenamiento (mín. recomendado 100)", GroupName = "Properties", Order = 0)]
public int TrainingPeriod
{ get; set; }
[Range(1, int.MaxValue), NinjaScriptProperty]
[Display(ResourceType = typeof(Custom.Resource), Name = "Lookback Period", Description = "Período de los indicadores que se utilicen como features", GroupName = "Properties", Order = 1)]
public int LookbackPeriod
{ get; set; }
[Range(1, int.MaxValue), NinjaScriptProperty]
[Display(ResourceType = typeof(Custom.Resource), Name = "Prediction Horizon", Description = "Número de barras hacia delante donde se realiza la predicción", GroupName = "Properties", Order = 2)]
public int PredictionHorizon
{ get; set; }
[Range(50, int.MaxValue), NinjaScriptProperty]
[Display(ResourceType = typeof(Custom.Resource), Name = "Prediction Loop", Description = "Informa cada cuántas barras debe ejecutarse el modelo: entrenamiento + predicción (mín. 50)", GroupName = "Properties", Order = 3)]
public int PredictionLoop
{ get; set; }
[Range(1, int.MaxValue), NinjaScriptProperty]
[Display(ResourceType = typeof(Custom.Resource), Name = "Number of Leaves", Description = "Controla la complejidad de cada árbol", GroupName = "Model Properties", Order = 0)]
public int NumberOfLeaves
{ get; set; }
[Range(1, int.MaxValue), NinjaScriptProperty]
[Display(ResourceType = typeof(Custom.Resource), Name = "Min Examples Per Leaf", Description = "Número mínimo de muestras por hoja (evita overfitting)", GroupName = "Model Properties", Order = 1)]
public int MinimumExampleCounterPerLeaf
{ get; set; }
[Range(0.0001, double.MaxValue), NinjaScriptProperty]
[Display(ResourceType = typeof(Custom.Resource), Name = "Learning Rate", Description = "Velocidad de aprendizaje del modelo (tradeoff entre rapidez y precisión)", GroupName = "Model Properties", Order = 2)]
public double LearningRate
{ get; set; }
#endregion
}
}