From 69a5d9de9f62625e39fa06f96b4306664f1b82db Mon Sep 17 00:00:00 2001 From: jsboige Date: Wed, 2 Sep 2026 21:18:59 +0200 Subject: [PATCH] feat(backtester,#7357): porter le corps SVM a noyau (tranche 4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remplace le stub explicite de TradingSvmModelConfig.TrainModel par le corps upstream du fork MyIntelligenceAgency/Lean (612dddf9) : ClassifierTradingModel, SvmTradingModel, GetKernel, cache statique, TrainModelInternal, SvmBenchmark et CalibrateComplexity. Substitution : AUCUNE. Le cadrage (geste 2) anticipait un remplacement Accord -> ML.NET/sklearn ; mesure faite, il est sans objet — ML.NET n'expose pas de SVM a noyau, et Accord.NET 3.8.2-alpha (la version du fork) restore, compile le corps verbatim et s'execute sur net9.0 avec 0 avertissement et pas de NU1701. La 2.6.0 validee par #12545 expose l'API precedente (KernelSupportVectorMachine + SequentialMinimalOptimization(...).Run()) et aurait impose une reecriture. Trois ecarts deliberes, documentes en tete de fichier : 1. `using DotNetNuke.Services.Log.EventLog` supprime (0 usage dans le corps) — abandon des DLL Aricie PKP, Option C tranchee le 2026-07-19. 2. Ordre des membres de KnownKernel : celui deja committe en tranche 3, pas celui de l'upstream. Reordonner changerait silencieusement les valeurs entieres de toute config deja serialisee (test d'epinglage ajoute). 3. CalibrateComplexity repare. Le bloc sous limite de temps upstream remplacait `teacher` au lieu d'appeler Learn : `machine` restait null, la NRE etait avalee, testError ne quittait jamais double.MaxValue et la methode rendait TOUJOURS son amorce 0.0001 — sous un log affirmant « SVM complexity calibrated ». Mesure cote a cote sur le meme jeu : upstream 0.0001, repare 706.88. Tests : 19/19 verts. Chaque assertion positive est appariee a une mutation qui ne change QUE le champ teste et doit la faire tomber (noyau lineaire sur XOR). La garde de l'ecart 3 est posee sur un XOR a paquets chevauchants, parce que sur un XOR net les deux corps rendent 0.0001 et la garde serait aveugle. See #7357 Co-Authored-By: Claude-Code --- .../TradingModelsConfigTests.cs | 45 +- .../TradingSvmModelConfigTests.cs | 265 ++++++++++ .../MyIA.Trading.Backtester.csproj | 17 +- .../TradingSvmModelConfig.cs | 486 +++++++++++++++++- 4 files changed, 788 insertions(+), 25 deletions(-) create mode 100644 MyIA.Trading.Backtester.Tests/TradingSvmModelConfigTests.cs diff --git a/MyIA.Trading.Backtester.Tests/TradingModelsConfigTests.cs b/MyIA.Trading.Backtester.Tests/TradingModelsConfigTests.cs index 35fae59307..a915e6f446 100644 --- a/MyIA.Trading.Backtester.Tests/TradingModelsConfigTests.cs +++ b/MyIA.Trading.Backtester.Tests/TradingModelsConfigTests.cs @@ -8,9 +8,8 @@ namespace MyIA.Trading.Backtester.Tests { /// - /// Tranche 3 (EPIC #7357 geste 3) : couche ML config portee. + /// Tranche 3 (EPIC #7357 geste 3) : couche ML config portee, mise a jour tranche 4. /// - cablage TradingModelsConfig.ModelType -> CurrentModelConfig (AutoML / Svm) - /// - garde explicite du port SVM en attente (tranche 4) : ApplicationException, pas d'echec silencieux /// - formats GetModelName des deux configs (logique upstream verbatim) /// - AutoMlTradingModel.Predict de bout en bout : pipeline ML.NET inline + prediction /// sur samples separables (voie reelle du port, pas un mock) @@ -31,18 +30,20 @@ public void ModelType_MulticlassSvm_ReturnsSvmConfig() Assert.IsType(config.CurrentModelConfig); } + /// + /// Tranche 4 : la garde « port en attente » a disparu avec le stub — TrainModel + /// entraine desormais un vrai SVM a noyau. La substance de ce port est couverte + /// par TradingSvmModelConfigTests ; ce qui reste a verifier ici est que plus + /// aucune ApplicationException de port pending ne subsiste dans le type. + /// [Fact] - public void SvmModelConfig_TrainModel_ThrowsExplicitPendingPortException() + public void SvmModelConfig_NoLongerCarriesThePendingPortGuard() { var config = new TradingSvmModelConfig(); - var dataConfig = BuildDataConfig(); - double testError = -1; - var ex = Assert.Throws( - () => config.TrainModel(Console.WriteLine, dataConfig, ref testError)); - - Assert.Contains("tranche 4", ex.Message); - Assert.Contains("AutoML", ex.Message); + // Le noyau par defaut est instanciable : le chemin d'entrainement est cable, + // il ne leve plus « port SVM en attente ». + Assert.NotNull(config.GetKernel(config.Kernel)); } [Fact] @@ -64,18 +65,30 @@ public void GetModelName_Svm_ContainsKernelAndComplexity() Assert.Contains("-kernel-InverseMultiquadric-complexity--1-Model.bin", name); } + /// + /// La delegation TradingTrainingConfig -> ModelsConfig.CurrentModelConfig etait + /// prouvee en tranche 3 par l'exception du stub. Le stub porte, elle se prouve + /// sur GetModelName, qui emprunte exactement le meme aiguillage CurrentModelConfig + /// et reste deterministe (pur calcul de chaine, aucune lecture disque). + /// [Fact] - public void TradingTrainingConfig_TrainModel_DelegatesToCurrentModelConfig() + public void TradingTrainingConfig_DelegatesToCurrentModelConfig() { - var trainingConfig = new TradingTrainingConfig + var svmConfig = new TradingTrainingConfig { + DataConfig = BuildDataConfig(), ModelsConfig = new TradingModelsConfig { ModelType = TradingModelType.MulticlassSvm } }; - double testError = -1; + var autoMlConfig = new TradingTrainingConfig + { + DataConfig = BuildDataConfig(), + ModelsConfig = new TradingModelsConfig { ModelType = TradingModelType.AutoML } + }; - // La delegation doit surfacer la garde explicite du port SVM pending, pas l'etouffer. - Assert.Throws( - () => trainingConfig.TrainModel(Console.WriteLine, ref testError)); + // Le nom rendu est celui de la config selectionnee, pas un defaut commun : + // l'aiguillage est donc bien traverse. + Assert.Contains("-kernel-", svmConfig.GetModelName()); + Assert.Contains("-AutoML-", autoMlConfig.GetModelName()); } [Fact] diff --git a/MyIA.Trading.Backtester.Tests/TradingSvmModelConfigTests.cs b/MyIA.Trading.Backtester.Tests/TradingSvmModelConfigTests.cs new file mode 100644 index 0000000000..0cf0c0b197 --- /dev/null +++ b/MyIA.Trading.Backtester.Tests/TradingSvmModelConfigTests.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Accord.MachineLearning.VectorMachines; +using Accord.MachineLearning.VectorMachines.Learning; +using Accord.Statistics.Analysis; +using Accord.Statistics.Kernels; +using Xunit; + +namespace MyIA.Trading.Backtester.Tests +{ + /// + /// Tranche 4 (EPIC #7357 geste 3) : corps SVM porte depuis le fork upstream. + /// Ces tests portent sur la SUBSTANCE du port — que le SVM a noyau apprenne + /// reellement une frontiere non lineairement separable — et pas seulement sur le + /// cablage. Chaque assertion positive est appariee a une mutation qui ne change + /// QUE le champ teste (le noyau), et qui doit la faire tomber : un test qui ne + /// peut pas echouer ne mesure rien. + /// + public sealed class TradingSvmModelConfigTests + { + /// + /// XOR : quatre paquets aux coins, classe 1 sur une diagonale, classe 0 sur + /// l'autre. Aucun hyperplan ne les separe — c'est precisement le probleme que + /// l'astuce du noyau resout, et le cas degenere que Prong B interdit d'eviter. + /// + private static List BuildXorSamples() + { + var samples = new List(); + var offsets = new[] { -0.06, -0.02, 0.02, 0.06 }; + foreach (var dx in offsets) + { + foreach (var dy in offsets) + { + samples.Add(BuildSample(0.0 + dx, 0.0 + dy, 0)); + samples.Add(BuildSample(1.0 + dx, 1.0 + dy, 0)); + samples.Add(BuildSample(0.0 + dx, 1.0 + dy, 1)); + samples.Add(BuildSample(1.0 + dx, 0.0 + dy, 1)); + } + } + return samples; + } + + private static TradingTrainingSample BuildSample(double x, double y, int label) + { + return new TradingTrainingSample + { + Inputs = new List { x, y }, + Output = label + }; + } + + /// + /// Bruit deterministe et portable. Volontairement PAS System.Random : son + /// algorithme n'est pas garanti stable d'une version de runtime a l'autre, et un + /// test de calibration doit rendre le meme nombre demain. + /// + private static double Jitter(int i, int salt, double spread) + { + unchecked + { + uint h = (uint)(i * 2654435761u + salt * 40503u); + h ^= h >> 13; h *= 2246822519u; h ^= h >> 16; + return ((h % 1000) / 1000.0 - 0.5) * spread; + } + } + + /// + /// XOR aux paquets ELARGIS jusqu'au chevauchement (spread 1.1). C'est la seule + /// famille sur laquelle la calibration a quelque chose a calibrer : sur un XOR + /// net, mesure, la complexite d'amorce 0.0001 classe deja parfaitement, donc la + /// conserver est le bon resultat — et une garde posee la-dessus serait aveugle. + /// + private static List BuildOverlappingXorSamples() + { + var samples = new List(); + const double spread = 1.1; + for (int i = 0; i < 30; i++) + { + void Add(double cx, double cy, int label, int salt) + { + samples.Add(BuildSample(cx + Jitter(i, salt, spread), cy + Jitter(i, salt + 1, spread), label)); + } + Add(0, 0, 0, 1); Add(1, 1, 0, 3); Add(0, 1, 1, 5); Add(1, 0, 1, 7); + } + return samples; + } + + private static TradingTrainTestData BuildOverlappingXorData() + { + var samples = BuildOverlappingXorSamples(); + return new TradingTrainTestData(samples.Count, samples.Count) + { + Training = samples, + Test = samples + }; + } + + private static MulticlassSupportVectorMachine Learn(IKernel kernel, double complexity, double[][] x, int[] y) + { + var teacher = new MulticlassSupportVectorLearning() + { + Learner = (p) => new SequentialMinimalOptimization() + { + Complexity = complexity, + UseKernelEstimation = true, + Kernel = kernel + } + }; + return teacher.Learn(x, y); + } + + /// + /// Le port entraine un vrai SVM a noyau : sur XOR, l'InverseMultiquadric du + /// fork atteint 0 erreur, et le modele est consommable via l'enveloppe + /// ITradingModel portee (ClassifierTradingModel.Predict). + /// + [Fact] + public void SvmTradingModel_LearnsXorAndPredictsThroughTradingModelWrapper() + { + var config = new TradingSvmModelConfig(); + var samples = BuildXorSamples(); + var x = samples.GetInputMatrix(); + var y = samples.GetOutputClasses(); + + var machine = Learn(config.GetKernel(KnownKernel.InverseMultiquadric), 1.0, x, y); + + Assert.Equal(0.0, GeneralConfusionMatrix.Estimate(machine, x, y).Error, 10); + + // L'enveloppe portee doit rendre les memes decisions, echantillon par echantillon. + var model = new SvmTradingModel { Svm = machine }; + var predicted = model.Predict(samples); + + Assert.Equal(samples.Count, predicted.Count); + for (int i = 0; i < samples.Count; i++) + { + Assert.Equal(samples[i].Output, predicted[i].Output); + } + } + + /// + /// MUTATION DE CONTROLE du test precedent : meme donnees, meme pipeline, meme + /// complexite — seul le noyau change (Linear). XOR n'etant pas lineairement + /// separable, l'erreur DOIT etre non nulle. Sans ce negatif, le test ci-dessus + /// serait compatible avec un probleme trivial ou n'importe quoi passe. + /// + [Fact] + public void LinearKernel_FailsOnXor_ProvingTheKernelIsWhatCarriesTheResult() + { + var samples = BuildXorSamples(); + var x = samples.GetInputMatrix(); + var y = samples.GetOutputClasses(); + + var machine = Learn(new Linear(), 1.0, x, y); + + Assert.True( + GeneralConfusionMatrix.Estimate(machine, x, y).Error > 0.0, + "Un noyau lineaire ne peut pas separer XOR : une erreur nulle signalerait un probleme degenere."); + } + + /// + /// GetKernel est la surface de substitution du port : chaque membre de + /// KnownKernel doit rendre le noyau Accord correspondant, et un membre hors + /// domaine doit lever plutot que retomber silencieusement sur un defaut. + /// + [Fact] + public void GetKernel_MapsEveryKnownKernelMemberToItsAccordCounterpart() + { + var config = new TradingSvmModelConfig(); + + Assert.IsType(config.GetKernel(KnownKernel.InverseMultiquadric)); + Assert.IsType(config.GetKernel(KnownKernel.NormalizedPolynomial3)); + Assert.IsType(config.GetKernel(KnownKernel.Polynomial3)); + Assert.IsType(config.GetKernel(KnownKernel.TStudent2)); + + // Tous les membres declares sont couverts : si quelqu'un en ajoute un sans + // etendre GetKernel, ce compte tombe et le test le dit. + Assert.Equal(4, Enum.GetValues(typeof(KnownKernel)).Length); + + Assert.Throws(() => config.GetKernel((KnownKernel)999)); + } + + /// + /// Les degres passes aux noyaux parametres sont ceux que le NOM du membre + /// annonce. Une inversion Polynomial(3)/TStudent(2) serait invisible au test + /// de type ci-dessus. + /// + [Fact] + public void GetKernel_UsesTheDegreeAnnouncedByTheMemberName() + { + var config = new TradingSvmModelConfig(); + + Assert.Equal(3, ((Polynomial)config.GetKernel(KnownKernel.Polynomial3)).Degree); + Assert.Equal(3, ((NormalizedPolynomial)config.GetKernel(KnownKernel.NormalizedPolynomial3)).Degree); + Assert.Equal(2, ((TStudent)config.GetKernel(KnownKernel.TStudent2)).Degree); + } + + /// + /// L'ordre des membres de KnownKernel est celui deja committe en tranche 3, PAS + /// celui de l'upstream (qui place TStudent2 en 2e position). Ce test epingle les + /// valeurs entieres : les reordonner casserait toute config deja serialisee, et + /// le ferait silencieusement. Ecart delibere 2, cf en-tete de TradingSvmModelConfig. + /// + [Fact] + public void KnownKernel_OrdinalsArePinned_BecauseTheyAreSerialised() + { + Assert.Equal(0, (int)KnownKernel.InverseMultiquadric); + Assert.Equal(1, (int)KnownKernel.NormalizedPolynomial3); + Assert.Equal(2, (int)KnownKernel.Polynomial3); + Assert.Equal(3, (int)KnownKernel.TStudent2); + } + + /// + /// GARDE DE REGRESSION DE L'ECART 3. Le corps upstream de CalibrateComplexity + /// remplacait `teacher` dans le bloc sous limite de temps sans jamais appeler + /// Learn : `machine` restait null, la NRE suivante etait avalee, testError ne + /// bougeait jamais de double.MaxValue, et la methode renvoyait TOUJOURS sa + /// valeur initiale 0.0001 — sous un log affirmant « SVM complexity calibrated ». + /// + /// La garde est posee sur des donnees ou elle DISCRIMINE, ce qui a ete mesure et + /// non suppose : sur ce jeu, le corps repare rend 706.88 et le corps upstream + /// 0.0001 ; sur un XOR net les deux rendent 0.0001 et la garde ne prouverait + /// rien. Un controle qu'on n'a pas vu echouer ne controle pas. + /// + [Fact] + public void CalibrateComplexity_ActuallyExploresAndDoesNotReturnItsSeedValue() + { + var config = new TradingSvmModelConfig(); + + var complexity = config.CalibrateComplexity( + BuildOverlappingXorData(), config.GetKernel(KnownKernel.InverseMultiquadric)); + + Assert.True( + complexity > 0.0001, + $"CalibrateComplexity a rendu {complexity}, sa valeur d'amorce : le bloc d'entrainement n'a pas tourne (defaut upstream, ecart 3)."); + } + + /// + /// La calibration doit AMELIORER ce qu'elle optimise. Le score de + /// TradingModelConfig.TestModel (bad - good, plus bas = meilleur) doit etre + /// strictement meilleur a la complexite calibree qu'a la complexite d'amorce — + /// sans quoi la boucle explorerait sans jamais choisir. + /// + [Fact] + public void CalibratedComplexity_ScoresBetterThanTheSeedComplexity() + { + var config = new TradingSvmModelConfig(); + var data = BuildOverlappingXorData(); + var x = data.Training.GetInputMatrix(); + var y = data.Training.GetOutputClasses(); + var kernel = config.GetKernel(KnownKernel.InverseMultiquadric); + + var calibrated = config.CalibrateComplexity(data, kernel); + + double seedScore = TradingModelConfig.TestModel( + data, new ClassifierTradingModel { Classifier = Learn(kernel, 0.0001, x, y) }); + double calibratedScore = TradingModelConfig.TestModel( + data, new ClassifierTradingModel { Classifier = Learn(kernel, calibrated, x, y) }); + + Assert.True( + calibratedScore < seedScore, + $"score calibre {calibratedScore} vs amorce {seedScore} : la calibration n'a rien ameliore."); + } + } +} diff --git a/MyIA.Trading.Backtester/MyIA.Trading.Backtester.csproj b/MyIA.Trading.Backtester/MyIA.Trading.Backtester.csproj index c1770cfc5c..b33bf5b963 100644 --- a/MyIA.Trading.Backtester/MyIA.Trading.Backtester.csproj +++ b/MyIA.Trading.Backtester/MyIA.Trading.Backtester.csproj @@ -5,7 +5,10 @@ vers CoursIA net9.0 — EPIC #7357 geste 3, tranche 1 (scaffold). Substitutions mesurees (docs/reference/backtester-e2-cadrage.md, geste 2) : - HintPath DLL Aricie/DNN : abandonnees, socle = MyIA.AI.Shared - - Accord.MachineLearning 3.8.2-alpha : remplace par ML.NET (mapping tranche suivante) + - Accord.MachineLearning 3.8.2-alpha : CONSERVE (tranche 4). Le cadrage anticipait une + substitution vers ML.NET ; mesure faite, elle est sans objet — ML.NET n'expose pas de + SVM a noyau, et Accord 3.8.2-alpha restore/compile/execute tel quel sur net9.0 (0 + avertissement, pas de NU1701). Verdict SOTA-OK, cf docs/reference/backtester-e2-svm-kernel.md. - Microsoft.ML.AutoML 0.20.1 : paire preview 0.24.0-preview.26160.2 (seule paire teste bout-en-bout, cadrage 2.2), figee par le test d'integration MyIA.Trading.Backtester.Tests Les PackageReferences Flee/Markdig seront cablees par les tranches portant les fichiers @@ -26,6 +29,18 @@ + + + + + + + + diff --git a/MyIA.Trading.Backtester/TradingSvmModelConfig.cs b/MyIA.Trading.Backtester/TradingSvmModelConfig.cs index 5167c8111e..9242a20790 100644 --- a/MyIA.Trading.Backtester/TradingSvmModelConfig.cs +++ b/MyIA.Trading.Backtester/TradingSvmModelConfig.cs @@ -1,15 +1,38 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Xml.Serialization; +using Accord.MachineLearning; +using Accord.MachineLearning.VectorMachines; +using Accord.MachineLearning.VectorMachines.Learning; +using Accord.Statistics.Analysis; +using Accord.Statistics.Kernels; +using Newtonsoft.Json; namespace MyIA.Trading.Backtester { - // Port minimal tranche 3 (EPIC #7357 geste 3) : la logique de nommage est le code - // upstream verbatim (Kernel/Complexity inclus), mais l'entrainement SVM n'est pas - // encore porte — la substitution kernel-SVM Accord -> ML.NET/sklearn (cadrage - // geste 2, docs/reference/backtester-e2-cadrage.md) fera l'objet de la tranche 4. - // L'exception est explicite pour qu'aucun appelant ne puisse confondre ce stub - // avec un entrainement reussi. + // Port tranche 4 (EPIC #7357) : le corps SVM du fork MyIntelligenceAgency/Lean + // (branche MyIABacktesting_integration, sha 612dddf9) est porte ici. La tranche 3 + // avait laisse TrainModel en stub explicite ; il entraine desormais reellement. + // + // Substitution : AUCUNE. Le SVM a noyau du fork tourne tel quel sur net9.0 avec + // Accord.NET 3.8.2-alpha (la version du fork) — restore + build + execution + // verifies (voir docs/reference/backtester-e2-svm-kernel.md et le corps de la PR). + // C'est la resolution de l'hypothese « Accord -> ML.NET/sklearn » du commentaire + // de tranche 3 : ML.NET n'a pas de SVM a noyau, et il n'a pas eu a en fournir un. + // + // Trois ecarts DELIBERES avec l'upstream, tous documentes ici : + // 1. `using DotNetNuke.Services.Log.EventLog` supprime — import jamais utilise + // dans le corps (1 seule occurrence : le using lui-meme). C'est l'abandon des + // 6 DLL Aricie PKP prescrit par l'Option C tranchee par le user le 2026-07-19. + // 2. L'ordre des membres de `KnownKernel` est celui deja committe en tranche 3, + // PAS celui de l'upstream (qui place TStudent2 en 2e). Reordonner changerait + // silencieusement la valeur entiere de chaque membre pour toute config deja + // serialisee : le nom est stable, l'ordinal ne l'est pas. + // 3. `CalibrateComplexity` : le bloc sous limite de temps est repare. Voir le + // commentaire detaille sur la methode — en l'etat upstream elle ne calibre + // rien et renvoie toujours sa valeur initiale. public enum KnownKernel { InverseMultiquadric, @@ -18,6 +41,51 @@ public enum KnownKernel TStudent2 } + /// + /// Enveloppe autour d'un classifieur Accord quelconque : + /// convertit les echantillons de trading en matrice d'entrees, delegue la decision au + /// classifieur, et re-emballe les sorties dans des . + /// + public class ClassifierTradingModel : ITradingModel + { + + [XmlIgnore] + [JsonIgnore] + public IClassifier Classifier { get; set; } + + public IList Predict(IList inputs) + { + var inputsMatrix = inputs.GetInputMatrix(); + var output = Classifier.Decide(inputsMatrix); + + var toReturn = new List(inputs.Count); + for (var index = 0; index < inputs.Count; index++) + { + var sample = inputs[index]; + var newSample = new TradingTrainingSample() { Inputs = sample.Inputs, Sample = sample.Sample }; + + newSample.Output = output[index]; + toReturn.Add(newSample); + } + + return toReturn; + } + + } + + /// + /// Specialisation SVM multiclasse de . + /// + public class SvmTradingModel : ClassifierTradingModel + { + + public MulticlassSupportVectorMachine Svm + { + get => (MulticlassSupportVectorMachine)Classifier; + set => Classifier = value; + } + } + public class TradingSvmModelConfig : TradingModelConfig { @@ -30,12 +98,414 @@ public override string GetModelName(TradingTrainingDataConfig dataConfig) public override ITradingModel TrainModel(Action logger, TradingTrainingDataConfig dataConfig, ref double testError) { - throw new ApplicationException( - "TradingSvmModelConfig.TrainModel : port SVM en attente (EPIC #7357 tranche 4) — substitution kernel-SVM Accord->ML.NET/sklearn cadrée au geste 2. Utiliser TradingModelType.AutoML."); + var objSvm = TrainModelInternal(logger, dataConfig, ref testError); + if (objSvm != null) + { + return new SvmTradingModel() { Svm = objSvm }; + } + + return null; } public double Complexity { get; set; } = -1; public KnownKernel Kernel { get; set; } = KnownKernel.InverseMultiquadric; + + public IKernel GetKernel(KnownKernel objKnownKernel) + { + switch (objKnownKernel) + { + case KnownKernel.InverseMultiquadric: + return new InverseMultiquadric(); + case KnownKernel.NormalizedPolynomial3: + return new NormalizedPolynomial(3); + case KnownKernel.Polynomial3: + return new Polynomial(3); + case KnownKernel.TStudent2: + return new TStudent(2); + } + + throw new ApplicationException($"Kernel {objKnownKernel} not accounted for"); + } + + private static Dictionary> _CachedModels = new Dictionary>(); + + private MulticlassSupportVectorMachine TrainModelInternal(Action logger, TradingTrainingDataConfig dataConfig, ref double testingError) + { + + var modelName = GetModelName(dataConfig); + + MulticlassSupportVectorMachine toReturn = null; + var exceptionFileName = GetModelExceptionFileName(dataConfig); + if (File.Exists(exceptionFileName)) + { + logger($"Skipping previously failed Model: {exceptionFileName}"); + return null; + } + + if (File.Exists(modelName)) + { + logger($"Loading Saved Model: {modelName}"); + if (!_CachedModels.TryGetValue(modelName, out toReturn)) + { + toReturn = Accord.IO.Serializer.Load>(modelName); + _CachedModels[modelName] = toReturn; + } + } + + TradingTrainTestData data = null; + if (toReturn == null) + { + logger($"Training new Model: {modelName}"); + data = dataConfig.GetTrainingSets(logger); + var xTrain = data.Training.GetInputMatrix(); + var yTrain = data.Training.GetOutputClasses(); + var xTest = data.Test.GetInputMatrix(); + var yTest = data.Test.GetOutputClasses(); + + var objKernel = GetKernel(Kernel); + if (Complexity < 0) + { + Complexity = CalibrateComplexity(data, objKernel); + logger($"SVM complexity calibrated: {Complexity}"); + } + else + { + logger($"SVM complexity defined : {Complexity}"); + } + + var teacher = new MulticlassSupportVectorLearning() + { + Learner = (p) => new SequentialMinimalOptimization() + { + Complexity = Complexity, + UseKernelEstimation = true, + Kernel = objKernel + } + }; + + var startTime = DateTime.Now; + bool completed = ExecuteWithTimeLimit(TrainingTimeout, () => + { + try + { + logger($"Learn start"); + toReturn = teacher.Learn(xTrain, yTrain); + } + catch (Exception e) + { + string exceptionMessage = e.ToString(); + WriteExceptionFile(logger, exceptionFileName, exceptionMessage); + toReturn = null; + } + }); + + if (!completed) + { + var exceptionMessage = $"Training timed out: {DateTime.Now.Subtract(startTime).TotalSeconds}s"; + WriteExceptionFile(logger, exceptionFileName, exceptionMessage); + toReturn = null; + } + + if (toReturn == null) + { + return toReturn; + } + + double trainError = GeneralConfusionMatrix.Estimate(toReturn, xTrain, yTrain).Error; + double testError = GeneralConfusionMatrix.Estimate(toReturn, xTest, yTest).Error; + logger($"SVM train error: {trainError} | test error: {testError}"); + + // Calibration probabiliste des sorties (Platt scaling) : requise pour que + // MulticlassSupportVectorMachine.Probabilities() soit exploitable, cf SvmBenchmark. + var ml = new MulticlassSupportVectorLearning() + { + Model = toReturn, + + Learner = (p) => new ProbabilisticOutputCalibration() + { + Model = p.Model + } + }; + ml.Learn(xTrain, yTrain); + + if (!File.Exists(modelName)) + { + (new FileInfo(modelName)).Directory.Create(); + Accord.IO.Serializer.Save>(toReturn, modelName); + } + + logger("SVM Training finished"); + } + + if (dataConfig.EnsureModelTested && testingError < 0) + { + if (data == null) + { + data = dataConfig.GetTrainingSets(logger); + } + testingError = TestModel(data, new ClassifierTradingModel() { Classifier = toReturn }); + } + + return toReturn; + + } + + /// + /// Diagnostic console : entraine le modele puis detaille, echantillon par echantillon, + /// les bonnes decisions, faux positifs, faux negatifs et decisions opposees. + /// + public void SvmBenchmark(TradingTrainingDataConfig dataConfig, Action logger) + { + + var bitcoinTrain = dataConfig.GetTrainingSets(logger); + + logger("Entering SVM Benchmark"); + double testingError = double.MinValue; + var machine = TrainModelInternal(logger, dataConfig, ref testingError); + if (machine == null) + { + logger("SVM Benchmark aborted: no model was learnt"); + return; + } + + var xTrain = bitcoinTrain.Training.GetInputMatrix(); + var yTrain = bitcoinTrain.Training.GetOutputClasses(); + var xTest = bitcoinTrain.Test.GetInputMatrix(); + var yTest = bitcoinTrain.Test.GetOutputClasses(); + + double trainError = GeneralConfusionMatrix.Estimate(machine, xTrain, yTrain).Error; + double testError = GeneralConfusionMatrix.Estimate(machine, xTest, yTest).Error; + + var ml = new MulticlassSupportVectorLearning() + { + Model = machine, + + Learner = (p) => new ProbabilisticOutputCalibration() + { + Model = p.Model + } + }; + ml.Learn(xTrain, yTrain); + + logger("SVM Training finished"); + + var decisions = machine.Decide(xTrain); + var predictionProbas = machine.Probabilities(xTrain); + int j = 1, k = 1, l = 1, m = 1; + for (int i = 0; i < decisions.Length; i++) + { + if (decisions[i] != yTrain[i]) + { + logger($"Bad Prediction {i}: {JsonConvert.SerializeObject(decisions[i])} vs y: {yTrain[i].ToString()} ({JsonConvert.SerializeObject(predictionProbas[i])})"); + j++; + } + } + + logger($"SVM Training Error: {trainError}"); + logger($"SVM test Error: {testError}"); + + decisions = machine.Decide(xTest); + predictionProbas = machine.Probabilities(xTest); + j = 1; + for (int i = 0; i < decisions.Length; i++) + { + if (decisions[i] != yTest[i]) + { + if (yTest[i] != 0) + { + if (decisions[i] == 0) + { + logger($"False Negative {i}: {JsonConvert.SerializeObject(decisions[i])} vs y: {yTest[i].ToString()} ({JsonConvert.SerializeObject(predictionProbas[i])})"); + j++; + } + else + { + logger($"Bad Decision {i}: {JsonConvert.SerializeObject(decisions[i])} vs y: {yTest[i].ToString()} ({JsonConvert.SerializeObject(predictionProbas[i])})"); + m++; + } + } + else + { + logger($"False Positive {i}: {JsonConvert.SerializeObject(decisions[i])} vs y: {yTest[i].ToString()} ({JsonConvert.SerializeObject(predictionProbas[i])})"); + k++; + } + } + else + { + if (decisions[i] != 0) + { + logger($"Good decision {i}: {JsonConvert.SerializeObject(decisions[i])} vs y: {yTest[i].ToString()} ({JsonConvert.SerializeObject(predictionProbas[i])})"); + l++; + } + } + } + + logger($"Good decisions {l} vs {j + k + m} = {m} Bad + {k} False Positive + {j} False Negative"); + + } + + /// + /// Recherche la complexite (parametre C du SMO) minimisant le score de test, par + /// exploration geometrique puis bissection entre les bornes retenues. + /// + /// + /// ECART DELIBERE AVEC L'UPSTREAM (ecart 3, cf en-tete de fichier). Dans le fork, le + /// bloc passe a se contentait de + /// re-instancier `teacher` sans jamais appeler `Learn` : + /// + /// () => { try { teacher = new MulticlassSupportVectorLearning<IKernel>(); } catch { } } + /// + /// `machine` restait donc `null` a chaque iteration, `machine.Decide(xTrain)` levait une + /// NullReferenceException aussitot avalee par le `catch` englobant, et `testError` + /// n'etait jamais mis a jour : `testError < currentResult` etant toujours faux + /// (double.MaxValue), `bestComplexity` conservait sa valeur initiale. La methode + /// renvoyait donc TOUJOURS 0.0001, tout en journalisant « SVM complexity calibrated ». + /// Porter ce corps tel quel aurait livre une calibration qui n'en est pas une, sous un + /// message affirmant le contraire. L'appel d'entrainement evidemment voulu est retabli, + /// et l'absence de modele est traitee explicitement au lieu d'etre masquee par une NRE. + /// Le defaut upstream est signale a part (voir le corps de la PR). + /// + public double CalibrateComplexity(TradingTrainTestData objTrainingData, IKernel objKernel) + { + var xTrain = objTrainingData.Training.GetInputMatrix(); + var yTrain = objTrainingData.Training.GetOutputClasses(); + + double currentComplexity = 0.0001; + double maxComplexity = 1000D; + double minComplexity = currentComplexity; + double bestComplexity = currentComplexity; + + double currentResult = double.MaxValue; + Boolean maxedOut = false; + double testError = double.MaxValue; + var coef = 100D; + + for (int idx = 0; idx < 25; idx++) + { + var teacher = new MulticlassSupportVectorLearning(); + teacher.Learner = (p) => new SequentialMinimalOptimization() + { + Complexity = currentComplexity, + UseKernelEstimation = true, + Kernel = objKernel + }; + MulticlassSupportVectorMachine machine = null; + try + { + bool completed = ExecuteWithTimeLimit(TrainingTimeout, + () => + { + try + { + machine = teacher.Learn(xTrain, yTrain); + } + catch (Exception) + { + machine = null; + } + }); + if (!completed) + { + break; + } + + if (machine == null) + { + // Cette complexite ne produit pas de modele : la traiter comme un + // plafond atteint plutot que comme une amelioration. + maxedOut = true; + } + else + { + var ml = new MulticlassSupportVectorLearning() + { + Model = machine, + + Learner = (p) => new ProbabilisticOutputCalibration() + { + Model = p.Model + } + }; + ml.Learn(xTrain, yTrain); + + var trainDecisions = machine.Decide(xTrain); + int j = 0; + for (int i = 0; i < trainDecisions.Length; i++) + { + if (trainDecisions[i] != yTrain[i]) + { + j++; + } + } + if (j == 0) + { + maxedOut = true; + } + + testError = TestModel(objTrainingData, new ClassifierTradingModel() { Classifier = machine }); + } + } + catch (Exception) + { + maxedOut = true; + } + + if (testError <= currentResult) + { + if (currentComplexity >= bestComplexity) + { + if (testError < currentResult) + { + minComplexity = Math.Max(minComplexity, bestComplexity); + bestComplexity = currentComplexity; + currentResult = testError; + } + } + else + { + maxComplexity = Math.Min(maxComplexity, bestComplexity); + bestComplexity = currentComplexity; + currentResult = testError; + } + } + else + { + if (currentComplexity < bestComplexity) + { + minComplexity = Math.Max(minComplexity, currentComplexity); + } + else + { + if (maxedOut) + { + if (currentComplexity > bestComplexity) + { + maxComplexity = Math.Min(maxComplexity, currentComplexity); + } + } + } + } + + if (!maxedOut) + { + currentComplexity *= coef; + } + else + { + if (currentComplexity > bestComplexity) + { + currentComplexity = (bestComplexity + minComplexity) / 2; + } + else + { + currentComplexity = (bestComplexity + maxComplexity) / 2; + } + } + } + + return bestComplexity; + } + } }