瀏覽代碼

Ajout de la version4

Rémy 1 周之前
父節點
當前提交
66c9bdcfc0

+ 1 - 0
BourseV4/Config/DEBUG.py

@@ -0,0 +1 @@
+DEBUGpickle = 1

+ 39 - 0
BourseV4/Config/actions.txt

@@ -0,0 +1,39 @@
+AC.PA
+AI.PA
+AIR.PA
+MT.AS
+CS.PA
+BNP.PA
+EN.PA
+BVI.PA
+CAP.PA
+CA.PA
+ACA.PA
+BN.PA
+DSY.PA
+EDEN.PA
+ENGI.PA
+EL.PA
+ERF.PA
+RMS.PA
+KER.PA
+LR.PA
+OR.PA
+MC.PA
+ML.PA
+ORA.PA
+RI.PA
+PUB.PA
+RNO.PA
+SAF.PA
+SGO.PA
+SAN.PA
+SU.PA
+GLE.PA
+STLAP.PA
+STMPA.PA
+TEP.PA
+HO.PA
+TTE.PA
+VIE.PA
+DG.PA

+ 20 - 0
BourseV4/Config/paths.py

@@ -0,0 +1,20 @@
+import platform
+import os
+
+Path_Project = os.getcwd()
+
+Path_FichierAction = f"{Path_Project}/Config/actions.txt"
+Path_run = f"{Path_Project}/Run/"
+Path_Parent = f"{Path_run}Parent/"
+
+Path_EnEssai = f"{Path_Project}/EnEssai"
+
+def get_os():
+	if platform.system() == "Windows":
+		return "windows"
+	elif platform.system() == "Linux":
+		return "linux"
+	else:
+		return "autre"
+
+SysExploite = get_os()

+ 22 - 0
BourseV4/Model/Action.py

@@ -0,0 +1,22 @@
+"""
+Fonction qui sort la valeur a une date précise
+"""
+from Config.paths import *
+
+
+def listeAction():
+    try:
+        with open(Path_FichierAction, 'r') as file:
+            actions = [line.strip() for line in file.readlines()]
+        return actions
+    except:
+        print(f"Erreur listeAction: Le fichier {Path_FichierAction} est introuvable.")
+
+
+def NombreActionFichier():
+	try:
+		with open(Path_FichierAction, 'r') as file:
+			actions = [line.strip() for line in file.readlines()]
+			return (len(actions))
+	except:
+		print(f"Erreur NombreActionFichier: Le fichier {Path_FichierAction} est introuvable.")

+ 203 - 0
BourseV4/Model/Donnee.py

@@ -0,0 +1,203 @@
+from Config.paths import SysExploite
+
+if SysExploite == "linux" :
+	from Model.My_Redis import *
+	
+from Model.Action import *
+from Config.DEBUG import *
+from Model.profil import profile
+
+import yfinance as yf
+import pandas as pd
+import numpy as np
+from svgelements import Close
+from ta import add_all_ta_features
+from datetime import datetime, timedelta, date
+import json
+from bisect import bisect_left
+import pickle
+
+
+def to_date(x):
+	if isinstance(x, str):
+		return datetime.strptime(x, "%Y-%m-%d").date()
+	return x
+
+
+class CL_Donnee:
+	def __init__(self, date_debut, nb_jours, Parametre):
+		self.valeurCloture = {}
+		self.listeDonne = {}
+		self.listeAction = listeAction()
+		if DEBUGpickle == 1 :
+			UUID = Parametre.UUIDRAM(date_debut,nb_jours)
+			pickle_path = f"RAM_cache_{UUID}.pkl"
+			
+			if not os.path.exists(pickle_path):
+				with open(pickle_path, "wb") as f:
+					self.RAMDonne(date_debut, nb_jours, Parametre)
+					data = {"valeurCloture": self.valeurCloture,"listeDonne": self.listeDonne}
+					pickle.dump(data, f)
+			else :
+				with open(pickle_path, "rb") as f:
+					data = pickle.load(f)
+				
+				self.valeurCloture = data["valeurCloture"]
+				self.listeDonne = data["listeDonne"]
+			
+		else:
+			self.RAMDonne(date_debut, nb_jours, Parametre)
+		self.Preprocess()
+	
+	@profile
+	def RAMDonne(self, date_debut, nb_jours, Parametre):
+		
+		date_depart = datetime.strptime(date_debut, "%Y-%m-%d").date() - timedelta(days=Parametre.NbJourRecul * 2)
+		for action in self.listeAction:
+			self.listeDonne[action] = {}
+			self.valeurCloture[action] = {}
+			listejour = ListeJour(date_depart, nb_jours + Parametre.NbJourRecul * 2)
+			for date in listejour:
+				valeur = LireRedis(action, date)
+				if valeur is not None:
+					self.listeDonne[action][date] = json.loads(valeur)
+					self.valeurCloture[action][date] = json.loads(valeur).get('Close')
+	
+	@profile
+	def ValeurDate(self, Action, Date):
+		return self.valeurCloture[Action][Date]
+	
+	@profile
+	def ResultatGlobal(self, Date, nbJour):
+		listeJour = ListeJour(Date, nbJour)
+		Open = 0
+		Close = 0
+		for action in self.listeAction:
+			Open += self.LireUneDonnee(action, listeJour[0]).get('Open')
+			Close += self.LireUneDonnee(action, listeJour[-1]).get('Close')
+		return (Close) / Open
+	
+	@profile
+	def Preprocess(self):
+		self.dataTriee = {}
+		
+		for action, data in self.listeDonne.items():
+			jours = [to_date(d) for d in data.keys()]
+			valeurs = list(data.values())
+			
+			tri = sorted(zip(jours, valeurs))
+			
+			self.dataTriee[action] = {
+				"jours": [x[0] for x in tri],
+				"valeurs": [x[1] for x in tri]
+			}
+	
+	@profile
+	def LireUneDonnee(self, Action, Date):
+		cible = to_date(Date)
+		
+		data = self.dataTriee[Action]
+		jours = data["jours"]
+		valeurs = data["valeurs"]
+		
+		i = bisect_left(jours, cible)
+		
+		if i >= len(jours):
+			return valeurs[-1]
+		
+		return valeurs[i]
+	
+	@profile
+	def LireUneDonneeTry(self, Action, Date):
+		try:
+			return self.listeDonne[Action][Date]
+		except (KeyError, IndexError, TypeError):
+			return None
+	
+	@profile
+	def DecalerJour(self, Date, Horizon):
+		date_obj = datetime.strptime(Date, "%Y-%m-%d")
+		nouvelle_date = date_obj + timedelta(days=Horizon)
+		return nouvelle_date.strftime("%Y-%m-%d")
+	
+	@profile
+	def PreparationDonnee(self, Date, nbJourRecule, Analysis, Horizon, ListeActions):
+		
+		listeJour = self.ListeJourInverse(Date, nbJourRecule)
+		
+		ListeReturn = []
+		rendement_futur = []
+		for action in ListeActions:
+			
+			# ===== HISTORIQUE (comme avant) =====
+			for jour in listeJour:
+				res = self.LireUneDonnee(action, jour)
+				for param in Analysis:
+					ListeReturn.append(res.get(param))
+			
+			# ===== TARGET (nouveau) =====
+			prix_actuel = self.LireUneDonnee(action, Date).get("Close")
+			prix_futur = self.LireUneDonnee(action, self.DecalerJour(Date, Horizon)).get("Close")
+			
+			rendement = (prix_futur - prix_actuel) / prix_actuel
+			rendement_futur.append(rendement)
+		
+		# ===== reshape historique =====
+		historique_indicateurs = np.array(ListeReturn).reshape(
+			(1, len(ListeActions), nbJourRecule, len(Analysis))
+		)
+		
+		# ===== normalisation =====
+		mean = historique_indicateurs.mean(axis=(0, 2), keepdims=True)
+		std = historique_indicateurs.std(axis=(0, 2), keepdims=True)
+		historique_indicateurs = (historique_indicateurs - mean) / (std + 1e-8)
+		
+		# ===== reshape target =====
+		rendement_futur = np.array(rendement_futur).reshape((1, len(ListeActions)))
+		
+		return historique_indicateurs, rendement_futur
+	
+	def PreparationJours(self, StartDay, NBJoursEntrainement, Parametre, Var_listeAction):
+		liste_Entree = []
+		liste_Target = []
+		listeJours = ListeJour(StartDay, NBJoursEntrainement)
+		for jour in listeJours:
+			Entree, target = self.PreparationDonnee(jour, Parametre.NbJourRecul, Parametre.Analysis, Parametre.Horizon,
+			                                        Var_listeAction)
+			Entree = np.asarray(Entree)
+			target = np.asarray(target)
+			if Entree.ndim == 4 and Entree.shape[0] == 1:
+				Entree = Entree[0]
+			liste_Entree.append(Entree)
+			liste_Target.append(target)
+		Entree = np.stack(liste_Entree, axis=0)
+		target = np.stack(liste_Target, axis=0)
+		target = target.reshape(target.shape[0], target.shape[2])
+		resGlobal = self.ResultatGlobal(StartDay, NBJoursEntrainement)
+		return Entree, target, resGlobal, listeJours
+	
+	@profile
+	def ListeJourInverse(self, Date, NbJour):
+		liste = []
+		date_obj = datetime.strptime(Date, "%Y-%m-%d").date()
+		i = 1
+		while (i < NbJour + 1):
+			date_obj = date_obj + timedelta(days=-1)
+			valeur1 = self.LireUneDonneeTry("MT.AS", date_obj.strftime("%Y-%m-%d"))
+			valeur2 = self.LireUneDonneeTry("AC.PA", date_obj.strftime("%Y-%m-%d"))
+			if valeur1 is not None and valeur2 is not None:
+				liste.insert(0, date_obj.strftime("%Y-%m-%d"))
+				i = i + 1
+		return liste
+	
+	def DataPickelData(self, StartDay, NBJoursEntrainement, Parametre, Var_listeAction):
+		pickle_path = f"data_cache_{Parametre.UUIDData(StartDay, NBJoursEntrainement)}.pkl"
+		if os.path.exists(pickle_path):
+			with open(pickle_path, "rb") as f:
+				Entree, Target, resGlobal, listeJours = pickle.load(f)
+		else:
+			Entree, Target, resGlobal, listeJours = self.PreparationJours(StartDay, NBJoursEntrainement, Parametre,
+			                                                                Var_listeAction)
+			with open(pickle_path, "wb") as f:
+				pickle.dump((Entree, Target, resGlobal, listeJours), f)
+		return Entree, Target, resGlobal, listeJours

+ 93 - 0
BourseV4/Model/Entrainement.py

@@ -0,0 +1,93 @@
+from Model.Modele import *
+from Model.Portefeuille import *
+from Model.Parametre import *
+from Model.Donnee import *
+from Model.Evaluation import *
+from Model.profil import profile
+from Config.DEBUG import *
+
+from pathlib import Path
+import pickle
+import os
+
+
+@profile
+def Entrainement(StartDayEntrainement, NBJoursEntrainement, StartDayValidation, NBJoursValidation):
+	listeModel = []
+	Var_listeAction = listeAction()
+	Parametre = CL_Param()
+	
+	DonneeEntrainement = CL_Donnee(StartDayEntrainement, NBJoursEntrainement, Parametre)
+	DonneeValidation = CL_Donnee(StartDayValidation, NBJoursValidation, Parametre)
+	
+	Model = CL_Model(Parametre)
+	
+	DossierResultat = f"{Path_run}/"
+	NumeroTest = fc_NumeroTest(DossierResultat)
+	DossierResultatModel = f"{DossierResultat}Modele_{NumeroTest}"
+	
+	if DEBUGpickle == 1:
+		EntreeEntrainement, targetEntrainement, resGlobalEntrainement, listeJoursEntrainement = DonneeEntrainement.DataPickelData(
+			StartDayEntrainement, NBJoursEntrainement, Parametre, Var_listeAction)
+		EntreeValidation, targetValidation, resGlobalValidation, listeJoursValidation = DonneeValidation.DataPickelData(
+			StartDayValidation, NBJoursValidation, Parametre, Var_listeAction)
+	else:
+		EntreeEntrainement, targetEntrainement, resGlobalEntrainement, listeJoursEntrainement = DonneeEntrainement.PreparationJours(
+			StartDayEntrainement,NBJoursEntrainement, Parametre, Var_listeAction)
+		EntreeValidation, targetValidation, resGlobalValidation, listeJoursValidation = DonneeValidation.PreparationJours(
+			StartDayValidation, NBJoursValidation, Parametre, Var_listeAction)
+	
+	epochs = Model.fit(EntreeEntrainement, targetEntrainement, Parametre, EntreeValidation, targetValidation)
+	
+	NomModel = xxx(Model, Parametre, EntreeEntrainement, targetEntrainement, listeJoursEntrainement, DonneeEntrainement,
+	    DossierResultatModel, StartDayEntrainement, NBJoursEntrainement, resGlobalEntrainement)
+	xxx(Model, Parametre, EntreeValidation, targetValidation, listeJoursValidation, DonneeValidation,
+	    DossierResultatModel, StartDayValidation, NBJoursValidation, resGlobalValidation)
+	return NomModel,epochs
+	
+# TODO lui trouvé un vrai nom
+def xxx(Model, Parametre, Entree, target, listeJours, Donnee, DossierResultatModel, StartDay, NBJours, resGlobal):
+	Portefeuille = CL_Achat(Parametre)
+	resultat = Model.predict(Entree)
+	AfficheStat(resultat, target)
+	
+	Portefeuille.MarquetGlobal(resultat, listeJours, Parametre, Donnee)
+	
+	valeur = Portefeuille.getValueTotal(listeJours[-1], Donnee)
+	
+	print(f"Debug valeur : {valeur}")
+	if Path(DossierResultatModel).exists():
+		nom_Model = next(Path(DossierResultatModel).iterdir()).stem
+		nom_Model = f"{DossierResultatModel}/{nom_Model}"
+		nom_res = f"{nom_Model}.res"
+	else:
+		nom = Model.EnregistreModel(Parametre, DossierResultatModel, valeur)
+		nom_Model = f"{DossierResultatModel}/{nom}"
+		nom_res = f"{DossierResultatModel}/{nom}.res"
+		
+	ajouter_res(nom_res, StartDay, NBJours, valeur, resGlobal, Portefeuille)
+	
+	return nom_Model
+
+
+
+
+@profile
+def Experimente(ListeDesJourDExperimentation, NBJours, NomModel):
+	Var_listeAction = listeAction()
+	
+	PathModel = Path(NomModel).parent
+
+	
+	Parametre = CL_Param()
+	Parametre.RestaureParam(NomModel)
+	
+	Model = CL_Model(Parametre)
+	Model.RestaurationModel(NomModel)
+	
+	for StartDay in ListeDesJourDExperimentation:
+		Donnee = CL_Donnee(StartDay, NBJours, Parametre)
+		Entree, target, resGlobal, listeJours = Donnee.DataPickelData( StartDay, NBJours, Parametre, Var_listeAction)
+		
+		xxx(Model, Parametre, Entree, target, listeJours, Donnee, PathModel, StartDay, NBJours, resGlobal)
+	

+ 69 - 0
BourseV4/Model/Evaluation.py

@@ -0,0 +1,69 @@
+from Model.Parametre import *
+from Model.profil import profile
+
+import datetime as dt
+import uuid
+from pathlib import Path
+import shutil
+import os
+import json
+
+
+@profile
+def ajouter_res(nom, jour_depart, nb_jours, res, resGlobal, Portefeuille):
+	# Création du fichier s'il n'existe pas
+	if not os.path.exists(nom):
+		with open(nom, "w") as f:
+			pass  # juste créer le fichier vide
+
+	# Préparer les données à ajouter
+	donnees = {
+		"jour_depart": jour_depart,
+		"nb_jours": nb_jours,
+		"res": res,
+		"resGlobal": resGlobal,
+		"nbVente": Portefeuille.nbVente,
+		"nbAchat": Portefeuille.nbAchat,
+		"PerteMax": Portefeuille.PerteMax,
+		"nbJourGagnant": Portefeuille.JourGagnant,
+		"nbJourPerdant": Portefeuille.JourPerdant
+	}
+
+	# Ajouter les données au fichier au format JSON
+	with open(nom, "a") as f:
+		f.write(json.dumps(donnees) + "\n")
+
+
+@profile
+def fc_NumeroTest(path):
+	# Timestamp compact : AAAAMMJJ_HHMMSS
+	timestamp = dt.datetime.now().strftime("%Y%m%d_%H%M%S")
+
+	# Petit identifiant unique si plusieurs lancements simultanés
+	uid = uuid.uuid4().hex[:6]
+
+	# Nom final
+	nom_dossier = f"Modele_{timestamp}_{uid}"
+
+	return nom_dossier
+
+
+def signe(resultat, target):
+	signe_resultat = resultat > 0
+	signe_target = target > 0
+	
+	Egal_signe = signe_resultat == signe_target
+	return np.sum(Egal_signe)
+
+def Ecart(resultat, target):
+	diff = resultat - target
+	return np.mean(diff),np.max(diff),np.min(diff),np.std(diff)
+
+def AfficheStat(resultat, target):
+	nb_Egal = signe(resultat, target)
+	print(f"Signe : {nb_Egal}/{np.size(resultat)} soit : {nb_Egal / np.size(resultat)}")
+	MoyenEcart,maxEcart,minEcart,stdEcart = Ecart(resultat, target)
+	print(f"Ecart :: Moyenne : {MoyenEcart}, max :{maxEcart}, min : {minEcart}, std : {stdEcart}")
+
+
+

+ 326 - 0
BourseV4/Model/Modele.py

@@ -0,0 +1,326 @@
+from Model.profil import profile
+import Model.Portefeuille
+import Model.Parametre
+import Model.Donnee
+
+import random
+import numpy as np
+import os
+import copy
+
+import torch
+import torch.nn as nn
+import torch.optim as optim
+from torch.utils.data import DataLoader, TensorDataset
+
+
+class CL_Model(nn.Module):
+	def __init__(self, Parametre, hidden=64, d_model=64, n_heads=4):
+		super().__init__()
+		
+		self.temporal_encoder = nn.Sequential(
+			nn.Linear(Parametre.NbAnalysis, hidden),
+			nn.ReLU(),
+			nn.Linear(hidden, d_model)
+		)
+		
+		self.temporal_mixer = nn.Sequential(
+			nn.Conv1d(d_model, d_model, kernel_size=3, padding=1),
+			nn.ReLU(),
+			nn.Conv1d(d_model, d_model, kernel_size=3, padding=1)
+		)
+		
+		# inter-assets attention
+		self.asset_attention = nn.MultiheadAttention(
+			embed_dim=d_model,
+			num_heads=n_heads,
+			batch_first=True
+		)
+		
+		self.norm = nn.LayerNorm(d_model)
+		
+		self.head = nn.Sequential(
+			nn.Linear(d_model, d_model),
+			nn.ReLU(),
+			nn.Linear(d_model, 1),
+		)
+		
+		# ===== TRAINING =====
+		""""""
+		# self.optimizer = optim.Adam(self.parameters(), lr=Parametre.LearningRate)
+		self.optimizer = torch.optim.AdamW(self.parameters(), lr=1e-3, weight_decay=1e-4)
+		# self.scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor=0.1, patience=5)
+		self.scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(self.optimizer, T_max=50)
+		"""
+		self.optimizer = torch.optim.Adam(model.parameters(), lr=1e-2)
+		self.scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)
+		"""
+		
+		self.loss_fn = nn.HuberLoss(delta=1.0)  # nn.MSELoss()#nn.SmoothL1Loss() nn.HuberLoss()#
+		
+		self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+		print(f"le réseau s'éxécute sur {self.device}")
+		self.to(self.device)
+	
+	# =========================
+	# FORWARD
+	# =========================
+	def forward(self, x):
+		# (B, A, T, F)
+		B, A, T, F = x.shape
+		
+		# =========================
+		# 1. Temporal encoding par asset
+		# =========================
+		x = self.temporal_encoder(x)  # (B, A, T, D)
+		
+		# =========================
+		# 2. Temporal convolution (mixing local patterns)
+		# =========================
+		x = x.view(B * A, T, -1)
+		x = x.permute(0, 2, 1)  # (B*A, D, T)
+		x = self.temporal_mixer(x)
+		x = x.mean(dim=2)  # (B*A, D)
+		
+		x = x.view(B, A, -1)  # (B, A, D)
+		
+		# =========================
+		# 3. Inter-asset modeling
+		# =========================
+		residual = x
+		x, _ = self.asset_attention(x, x, x)
+		x = self.norm(x + residual)
+		
+		# =========================
+		# 4. Head regression
+		# =========================
+		x = self.head(x).squeeze(-1)  # (B, A)
+		
+		return x
+	
+	# =========================
+	# FIT
+	# =========================
+	@profile
+	def ancienfit(self, X, y, Parametre, EntreeValidation, TargetValidation):
+		
+		self.train()
+		
+		X = torch.tensor(X, dtype=torch.float32)
+		y = torch.tensor(y, dtype=torch.float32)
+		
+		dataset = TensorDataset(X, y)
+		loader = DataLoader(dataset, batch_size=Parametre.batch_size, shuffle=False)
+		best_val = float("inf")
+		nbStagne = 0
+		for epoch in range(Parametre.epochs):
+			total_loss = 0
+			self.train()
+			
+			self.optimizer.step()
+			self.scheduler.step()
+			
+			for X_batch, y_batch in loader:
+				X_batch = X_batch.to(self.device)
+				y_batch = y_batch.to(self.device)
+				self.optimizer.zero_grad()
+				
+				pred = self(X_batch)
+				
+				loss = self.loss_fn(pred, y_batch)
+				direction_loss = torch.mean(torch.relu(-pred * y_batch))
+				loss = loss + 2 * direction_penalty
+				
+				loss.backward()
+				
+				self.optimizer.step()
+				
+				total_loss += loss.item()
+			
+			avg_loss = total_loss / len(loader)
+			current_lr = self.optimizer.param_groups[0]['lr']
+			
+			validLoss = self.ValidLoss(EntreeValidation, TargetValidation, Parametre)
+			
+			if validLoss < best_val:
+				best_val = validLoss
+				nbStagne = 0
+				best_weights = copy.deepcopy(self.state_dict())
+				print(
+					f"epoch {epoch} | loss: {avg_loss:.6f} | ValidLoss : {validLoss} | Stagne : {nbStagne}| LR : {current_lr} ")
+			else:
+				nbStagne += 1
+				if nbStagne == 20:
+					self.load_state_dict(best_weights)
+				if nbStagne == 1000:
+					self.load_state_dict(best_weights)
+					return epoch
+	
+	@profile
+	def fit(self, X, y, Parametre, EntreeValidation, TargetValidation):
+		
+		self.train()
+		
+		X = torch.tensor(X, dtype=torch.float32)
+		y = torch.tensor(y, dtype=torch.float32)
+		
+		dataset = TensorDataset(X, y)
+		
+		loader = DataLoader(dataset, batch_size=Parametre.batch_size, shuffle=False)
+		
+		best_val = float("inf")
+		nbStagne = 0
+		best_weights = None
+		
+		for epoch in range(Parametre.epochs):
+			
+			total_loss = 0
+			self.train()
+			
+			for X_batch, y_batch in loader:
+				X_batch = X_batch.to(self.device)
+				y_batch = y_batch.to(self.device)
+				
+				self.optimizer.zero_grad()
+				
+				pred = self(X_batch)
+				
+				loss = self.CalculLoss(pred,y_batch)
+				
+				# stabilité gradients
+				norm = torch.nn.utils.clip_grad_norm_(self.parameters(), 5.0)
+			
+				loss.backward()
+				self.optimizer.step()
+				
+				total_loss += loss.item()
+			
+			avg_loss = total_loss / len(loader)
+			
+			# validation
+			validLoss = self.ValidLoss(EntreeValidation, TargetValidation, Parametre)
+			
+			current_lr = self.optimizer.param_groups[0]["lr"]
+			
+			self.scheduler.step()
+			
+			# early stopping
+			if validLoss < best_val:
+				best_val = validLoss
+				
+				best_weights = copy.deepcopy(self.state_dict())
+				print(
+					f"epoch {epoch} | "    f"loss: {avg_loss:.6f} | " f"valid: {validLoss:.6f} | "f"stagnation: {nbStagne} | "    f"lr: {current_lr}")
+				nbStagne = 0
+			else:
+				nbStagne += 1
+				if nbStagne >= 1000:
+					self.load_state_dict(best_weights)
+					return epoch
+	
+	@profile
+	def CalculLoss(self, X, Y):
+		loss = self.loss_fn(X,Y)
+		
+		direction_loss = torch.mean(torch.relu(-X * Y))
+		# print(direction_loss)
+		
+		loss = loss + direction_loss
+		return direction_loss
+	
+	# =========================
+	# PREDICT
+	# =========================
+	@profile
+	def predict(self, X):
+		self.eval()
+		with torch.no_grad():
+			X = torch.tensor(X, dtype=torch.float32).to(self.device)
+			pred = self(X)
+		
+		return pred.cpu().numpy()
+	
+	@profile
+	def EnregistreModel(self, Parametre, NomDossier, valeur):
+		if not os.path.exists(NomDossier):
+			os.makedirs(NomDossier)
+		
+		nom = valeur / Parametre.PortefeuilleDemarage
+		nom = str(nom).replace('.', '_')
+		
+		nom_model = f"{NomDossier}/{nom}.pt"
+		
+		torch.save({
+			"model_state_dict": self.state_dict(),
+			"optimizer_state_dict": self.optimizer.state_dict(),
+			"best_score": valeur
+		}, nom_model)
+		
+		nom_Parametre = f"{NomDossier}/{nom}"
+		Parametre.StockParam(nom_Parametre)
+		
+		return nom
+	
+	@profile
+	def RestaurationModel(self, NomDuModel):
+		NomDuModel = NomDuModel + ".pt"
+		checkpoint = torch.load(NomDuModel, map_location=self.device)
+		
+		self.load_state_dict(checkpoint["model_state_dict"])
+		self.optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
+		
+		self.to(self.device)
+		self.eval()
+		
+		return self
+	
+	@profile
+	def Evaluation(self, Date, Portefeuille, Donnee):
+		return Portefeuille.getValueTotal(Date, Donnee)
+	
+	
+	def ValidLoss(self, Entree, Target, Parametre):
+		
+		self.eval()
+		
+		X = torch.tensor(Entree, dtype=torch.float32)
+		y = torch.tensor(Target, dtype=torch.float32)
+		
+		dataset = TensorDataset(X, y)
+		
+		loader = DataLoader(
+			dataset,
+			batch_size=Parametre.batch_size,
+			shuffle=False
+		)
+		
+		total_loss = 0
+		
+		with torch.no_grad():
+			for X_batch, y_batch in loader:
+				X_batch = X_batch.to(self.device)
+				y_batch = y_batch.to(self.device)
+				
+				pred = self(X_batch)
+				
+				loss = self.CalculLoss(pred, y_batch)
+				
+				total_loss += loss.item()
+		
+		avg_loss = total_loss / len(loader)
+		
+		return avg_loss
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 127 - 0
BourseV4/Model/My_Redis.py

@@ -0,0 +1,127 @@
+from Config.paths import Path_FichierAction, SysExploite
+from Model.profil import profile
+
+
+
+import redis
+from datetime import datetime, timedelta, date
+import yfinance as yf
+from ta import add_all_ta_features
+import json
+
+import warnings
+warnings.simplefilter("ignore", FutureWarning)
+
+
+# Initialisation de Redis
+redis_client = redis.StrictRedis(host='localhost', port=6379, db=0)
+
+Gl_DebutEnresitrement = "2016-01-01"
+
+
+@profile
+def BaseOK():
+    nombre_cles = redis_client.dbsize()
+    # Vérifier si inférieur à 90 000
+    if nombre_cles < 90000:
+        EnregistreTout()
+
+
+@profile
+def EnregistreTout():
+    date_aujourdhui = datetime.now().strftime("%Y-%m-%d")
+    try:
+        # Lecture du fichier contenant les noms des actions
+        with open(Path_FichierAction, 'r') as file:
+            actions = [line.strip() for line in file.readlines()]
+
+        # Vérification que le fichier contient des actions
+        if not actions:
+            print("Le fichier ne contient aucune action à traiter.")
+            return
+
+        # Boucle sur toutes les actions pour appeler Enregistre
+        for action in actions:
+            Enregistre(action, Gl_DebutEnresitrement, date_aujourdhui)
+
+        print("Enregistrement terminé pour toutes les actions.")
+
+    except FileNotFoundError:
+        print(f"Erreur : Le fichier {Path_FichierAction} est introuvable.")
+    except Exception as e:
+        print(f"Erreur lors de l'exécution de EnregistreTout : {e}")
+
+
+@profile
+def Enregistre(Action, DateDebut, DateFin):
+    """
+    Télécharge les données d'une action sur une période donnée,
+    calcule les indicateurs techniques et les enregistre dans Redis.
+    """
+    try:
+        data = yf.download(Action, start=DateDebut, end=DateFin,auto_adjust=False)
+        if data.empty:
+            print("Aucune donnée récupérée pour cette période.")
+            return
+        data.columns = data.columns.droplevel(1)
+        # Ajout des colonnes avec des indicateurs techniques
+        data = add_all_ta_features(
+            data, open="Open", high="High", low="Low", close="Close", volume="Volume", fillna=True
+        )
+
+        # Convertir les données en dictionnaire
+        data_dict = data.to_dict(orient='index')
+
+        # Enregistrement dans Redis
+        for date, valeurs in data_dict.items():
+            key = f"{Action}:{date.strftime('%Y-%m-%d')}"
+            redis_client.set(key, json.dumps(valeurs))  # Sérialise les données en JSON pour les stocker
+
+
+    except Exception as e:
+        print(f"Erreur lors de l'enregistrement des données de {Action}: {e}")
+
+
+@profile
+def LireRedis(Action, Date):
+    cle = Action + ":" + str(Date)
+    return redis_client.get(cle)
+
+
+""""
+	Fonction qui renvoie la liste des jours pour la preparation des donnéee
+"""
+@profile
+def ListeJour(Date, NbJour):
+    liste = [Date]
+    if isinstance(Date, date):
+        date_obj = Date
+    else:
+        date_obj = datetime.strptime(Date, "%Y-%m-%d").date()
+    i = 1
+    while (i < NbJour):
+        date_obj = date_obj + timedelta(days=+1)
+        cle = "MT.AS" + ":" + date_obj.strftime("%Y-%m-%d")
+        valeur1 = redis_client.get(cle)
+        cle = "AC.PA" + ":" + date_obj.strftime("%Y-%m-%d")
+        valeur2 = redis_client.get(cle)
+        if valeur1 is not None and valeur2 is not None:
+            liste.append(date_obj.strftime("%Y-%m-%d"))
+            i = i + 1
+        else :
+            #print(f"erreur Liste jour : {date_obj}")
+            today = datetime.now().date()
+            if date_obj > today:
+                raise ValueError("La date est dans le futur")
+
+    return liste
+
+@profile
+def DernierJour(Date,Nbjour):
+	return ListeJour(Date,Nbjour)[-1]
+ 
+"""
+La fonction BaseOk est executer a chaque fois que l'on appel le fichier donnée
+"""
+if SysExploite == "linux" :
+    BaseOK()

+ 264 - 0
BourseV4/Model/Parametre.py

@@ -0,0 +1,264 @@
+from Model.Donnee import *
+from Model.profil import profile
+from Config.paths import SysExploite
+
+import datetime as dt
+import json
+import hashlib
+import os
+import uuid
+import random
+
+ProbabiliteMutationNombre = 0.5
+ProbabiliteMutationReste = 0.5
+
+
+class CL_Param:
+	def __init__(self):
+		
+		self.PortefeuilleDemarage = 50000.0
+		self.NBAction = NombreActionFichier()
+		
+		"""
+		L'horizon depent directement du nombre de jour de recul la regle epirique est : lookback ≈ 3× à 10× horizon
+		
+					| Fast  | a testé   | Optimal   | Robuste   |
+		NBjourRecul |  5    |  32 jours | 64        | 100       |
+		horizon     | 1     | 5 jours   | 10        | 15        |
+		
+		"""
+		self.NbJourRecul = 32
+		self.Horizon = 5
+		
+		self.Analysis =  ["Close","others_dr","trend_sma_fast","trend_sma_slow","momentum_rsi","volatility_atr","volatility_bbw","volume_obv"]
+		self.NbAnalysis = len(self.Analysis)
+		self.poidsAchat = [0.25, 0.20, 0.15, 0.10, 0.05]
+		self.NBachat = len(self.poidsAchat)
+		self.ValVente = 0
+		if SysExploite == "windows":
+			self.epochs = 2
+		else :
+			self.epochs = 20000
+		self.batch_size = 128
+		
+		self.LearningRate = 1e-3
+	
+		# Groupes pour éviter les features corrélés
+		self.groups = {
+			# ---- MACD ----
+			"macd": {
+				"trend_macd": 5,
+				"trend_macd_signal": 5,
+				"trend_macd_diff": 5
+			},
+			
+			# ---- PPO ----
+			"ppo": {
+				"momentum_ppo": 5,
+				"momentum_ppo_signal": 5,
+				"momentum_ppo_hist": 5
+			},
+			
+			# ---- Stoch RSI ----
+			"stoch_rsi": {
+				"momentum_stoch_rsi": 5,
+				"momentum_stoch_rsi_k": 5,
+				"momentum_stoch_rsi_d": 5
+			},
+			
+			# ---- Stochastic ----
+			"stoch": {
+				"momentum_stoch": 3,
+				"momentum_stoch_signal": 3
+			},
+			
+			# ---- ATR / Volatility ----
+			"atr": {
+				"volatility_atr": 5,
+				"volatility_ui": 1
+			},
+			
+			# ---- Bollinger ----
+			"bollinger": {
+				"volatility_bbm": 5,
+				"volatility_bbh": 5,
+				"volatility_bbl": 5,
+				"volatility_bbw": 5,
+				"volatility_bbp": 3,
+				"volatility_bbhi": 3,
+				"volatility_bbli": 3
+			},
+			
+			# ---- Keltner ----
+			"keltner": {
+				"volatility_kcc": 5,
+				"volatility_kch": 5,
+				"volatility_kcl": 5,
+				"volatility_kcw": 3,
+				"volatility_kcp": 3,
+				"volatility_kchi": 3,
+				"volatility_kcli": 3
+			},
+			
+			# ---- Donchian ----
+			"donchian": {
+				"volatility_dcl": 3,
+				"volatility_dch": 5,
+				"volatility_dcm": 3,
+				"volatility_dcw": 3,
+				"volatility_dcp": 3
+			},
+			
+			# ---- SMA / EMA ----
+			"moving_avg": {
+				"trend_sma_fast": 3,
+				"trend_sma_slow": 3,
+				"trend_ema_fast": 3,
+				"trend_ema_slow": 3
+			},
+			
+			# ---- ADX ----
+			"adx": {
+				"trend_adx": 5,
+				"trend_adx_pos": 3,
+				"trend_adx_neg": 3
+			},
+			
+			# ---- Vortex ----
+			"vortex": {
+				"trend_vortex_ind_pos": 3,
+				"trend_vortex_ind_neg": 3,
+				"trend_vortex_ind_diff": 5
+			},
+			
+			# ---- Aroon ----
+			"aroon": {
+				"trend_aroon_up": 3,
+				"trend_aroon_down": 3,
+				"trend_aroon_ind": 5
+			},
+			
+			# ---- Ichimoku ----
+			"ichimoku": {
+				"trend_ichimoku_conv": 3,
+				"trend_ichimoku_base": 3,
+				"trend_ichimoku_a": 3,
+				"trend_ichimoku_b": 3,
+				"trend_stc": 3
+			},
+			
+			# ---- Volume indicators ----
+			"volume": {
+				"volume_obv": 5,
+				"volume_mfi": 5,
+				"volume_cmf": 5,
+				"volume_vpt": 3,
+				"volume_vwap": 3,
+				"volume_adi": 3,
+				"volume_fi": 1,
+				"volume_em": 1,
+				"volume_nvi": 1
+			},
+			
+			# ---- Momentum divers ----
+			"momentum_other": {
+				"momentum_rsi": 5,
+				"momentum_wr": 3,
+				"momentum_uo": 3,
+				"momentum_ao": 3,
+				"momentum_kama": 3,
+				"momentum_tsi": 3
+			},
+			
+			# ---- Trend divers ----
+			"trend_other": {
+				"trend_trix": 3,
+				"trend_mass_index": 3,
+				"trend_kst": 3,
+				"trend_kst_sig": 3,
+				"trend_kst_diff": 3,
+				"trend_dpo": 3,
+				"trend_psar_up_indicator": 5,
+				"trend_psar_down_indicator": 5
+			},
+			
+			# ---- Faible pertinence ----
+			"low_value": {
+				"others_dr": 1,
+				"others_dlr": 1
+			}
+		}
+	
+	@profile
+	def StockParam(self, NomDuModel):
+		#Sauvegarde les paramètres dans un fichier .param
+		data = {
+			"NbJourRecul": self.NbJourRecul,
+			"Horizon":self.Horizon,
+			"NBAction": self.NBAction,
+			"Analysis": self.Analysis,
+			"NbParam": self.NbAnalysis,
+			"PortefeuilleDemarage": self.PortefeuilleDemarage,
+			"poidsAchat":self.poidsAchat,
+			"ValVente": self.ValVente,
+			"epochs":self.epochs,
+			"batch_size":self.batch_size,
+			"LearningRate":self.LearningRate,
+		}
+		with open(f"{NomDuModel}.param", "w") as f:
+			json.dump(data, f, indent=4)
+	
+	@profile
+	def RestaureParam(self, NomDuModel):
+		#Restaure les paramètres depuis un fichier .param
+		NomDuModel = NomDuModel + ".param"
+		if not os.path.exists(NomDuModel):
+			raise FileNotFoundError(f"Le fichier {NomDuModel}.param est introuvable.")
+		
+		with open(NomDuModel, "r") as f:
+			data = json.load(f)
+		
+		self.NbJourRecul = data.get("NbJourRecul", self.NbJourRecul)
+		self.Horizon = data.get("Horizon", self.Horizon)
+		self.NBAction = data.get("NBAction", self.NBAction)
+		self.Analysis = data.get("Analysis", self.Analysis)
+		self.NbAnalysis = data.get("NbParam", len(self.Analysis))
+		self.PortefeuilleDemarage = data.get("PortefeuilleDemarage", self.PortefeuilleDemarage)
+		self.poidsAchat = data.get("poidsAchat", self.poidsAchat)
+		self.ValVente = data.get("ValVente", self.ValVente)
+		self.epochs = data.get("",self.epochs)
+		self.batch_size =data.get("",self.batch_size)
+		self.LearningRate =data.get("",self.LearningRate)
+	
+	def UUIDData(self, Demmarage, NBNJour):
+		data = {
+			"Demmarage": Demmarage,
+			"NBJour": NBNJour,
+			"NbJourRecul": self.NbJourRecul,
+			"Horizon": self.Horizon,
+			"NBAction": self.NBAction,
+			"Analysis": self.Analysis
+		}
+		
+		# sérialisation stable (ordre garanti)
+		payload = json.dumps(data, sort_keys=True).encode("utf-8")
+		
+		# hash
+		h = hashlib.blake2s(payload, digest_size=8).hexdigest()  # 16 chars
+		
+		return h
+	
+	def UUIDRAM(self, date_debut, nb_jours):
+		data = {
+			"Demmarage": date_debut,
+			"NBJour": nb_jours,
+			"NbJourRecul": self.NbJourRecul
+		}
+		
+		# sérialisation stable (ordre garanti)
+		payload = json.dumps(data, sort_keys=True).encode("utf-8")
+		
+		# hash
+		h = hashlib.blake2s(payload, digest_size=8).hexdigest()  # 16 chars
+		
+		return h

+ 89 - 0
BourseV4/Model/Portefeuille.py

@@ -0,0 +1,89 @@
+from Model.Donnee import *
+from Model.profil import profile
+
+
+class CL_Achat:
+	@profile
+	def __init__(self, Parametre):
+		self.portefeuille = [0] * Parametre.NBAction
+		self.cash = Parametre.PortefeuilleDemarage
+
+		self.fee = 0
+
+		self.ValueTotalVeille = Parametre.PortefeuilleDemarage
+
+		self.nbAchat = 0
+		self.nbVente = 0
+		self.PerteMax = 0
+		self.GainMaxJour = 0
+		self.JourGagnant = 0
+		self.JourPerdant = 0
+
+	@profile
+	def achat(self, valeurs,Parametre,date,Donnee): #TODO Optimisé pour voir si on achet plus ou moins il y a d'autres stratégie d'achat a voir
+		indices_tries = sorted(range(len(valeurs)), key=lambda i: valeurs[i], reverse=True)
+		for i,p in zip(indices_tries[:Parametre.NBachat],Parametre.poidsAchat):
+			if self.cash*p > 1000:
+				self.achatAction(i,self.cash*p , date, Donnee)
+		
+	def achatAction(self,action, montant, date, Donnee):
+		prix_total = montant + self.fee
+		nb = montant / Donnee.ValeurDate(Donnee.listeAction[action], date)
+		self.nbAchat += 1
+		self.cash -= prix_total
+		self.portefeuille[action] += nb
+
+	@profile
+	def vendsTout(self, action, date, Donnee):
+		if (self.portefeuille[action] > 0):
+			self.nbVente += 1
+			prix_total = self.portefeuille[action] * Donnee.ValeurDate(Donnee.listeAction[action], date) - self.fee
+			self.portefeuille[action] = 0
+			self.cash += prix_total
+
+	@profile
+	def getCash(self):
+		return self.cash
+
+	@profile
+	def getValueTotal(self, Date, Donnee):
+		r = self.cash
+		for a in range(len(self.portefeuille)):
+			if (self.portefeuille[a] > 0):
+				r += Donnee.ValeurDate(Donnee.listeAction[a], Date) * self.portefeuille[a]
+				
+		return r
+
+	@profile
+	def MarquetGlobal(self, resultat, listeJours, Parametre, Donnee):
+		resultat_np = resultat
+		
+		PerteCourante = 0
+		
+		for NJour, date in enumerate(listeJours):
+			valeurs = resultat_np[NJour]
+			
+			# Conditions vectorisées
+			mask_vente = valeurs < Parametre.ValVente #On vend tous ce qui est inférieur a 0
+			indices_vente = np.where(mask_vente)[0]
+
+			for i in indices_vente:
+				self.vendsTout(i, date, Donnee)
+			
+			self.achat(valeurs,Parametre,date,Donnee)
+
+			ValeurJour = self.getValueTotal(date, Donnee)
+			
+			if (ValeurJour > self.ValueTotalVeille):
+				PerteCourante = 0
+				self.JourGagnant += 1
+				gain = self.ValueTotalVeille - ValeurJour
+				if (self.GainMaxJour < gain):
+					self.GainMaxJour = gain
+			elif (ValeurJour < self.ValueTotalVeille):
+				self.JourPerdant += 1
+				PerteCourante -= self.ValueTotalVeille - ValeurJour
+				if (self.PerteMax > PerteCourante):
+					self.PerteMax = PerteCourante
+			self.ValueTotalVeille = ValeurJour
+			

+ 24 - 0
BourseV4/Model/infoReseau.py

@@ -0,0 +1,24 @@
+from Model.Modele import *
+from Model.Parametre import *
+from Model.Entrainement import *
+
+from torchinfo import summary
+from pathlib import Path
+
+parametre = CL_Param()
+model = CL_Model(parametre)
+
+pickle_path = next(Path("..").glob("data_cache_*"), None)
+
+
+#pickle_path = Path(r"..\data_cache_2025-02-17_200.pkl")
+
+
+if os.path.exists(pickle_path):
+	with open(pickle_path, "rb") as f:
+		Entree, Target, resGlobal, listeJours = pickle.load(f)
+else :
+	print("Pas trouvé")
+	
+print(Entree.shape)
+summary(model, Entree.shape)

+ 48 - 0
BourseV4/Model/main.py

@@ -0,0 +1,48 @@
+from Model.profil import profile, dump_stats
+from Config.paths import *
+from Model.Entrainement import *
+from Model.Evaluation import *
+
+
+import shutil
+from pathlib import Path
+from multiprocessing import Pool, cpu_count
+
+
+
+
+# 2018-03-01 : Marché globalement normal avec volatilité modérée, servant de référence de comportement standard.
+# 2019-06-03 : Régime haussier propre et stable, idéal pour apprendre une dynamique de marché saine.
+# 2020-02-20 : Point de départ du choc COVID, illustrant une rupture de régime brutale et extrême.
+# 2021-11-15 : Phase d’euphorie de fin de cycle avec signaux précoces de retournement.
+# 2022-06-13 : Marché baissier chaotique marqué par inflation, hausse des taux et forte volatilité.
+# 2024-01-15 : Reprise directionnelle propre après 2023, avec volatilité comprimée et flux réguliers.
+# 2024-04-19 : Marché en consolidation avec rotations sectorielles et bruit directionnel élevé.
+# 2024-09-02 : Début d’un stress macro latent entraînant des transitions fréquentes de tendance.
+# 2025-02-17 : Excès directionnel anormal avec tendance trop régulière, propice au surapprentissage.
+# 2025-06-03 : Phase de normalisation post-excès, retrouvant un comportement de marché équilibré.
+
+StartDayEntreainement = "2017-11-09"
+#NBJoursEntrainement = 2000 # soit le dernier jour : 2025-09-03
+#NBJoursEntrainement = 1800 # soit le dernier jour : 2024-11-19
+NBJoursEntrainement = 1500 # soit le dernier jour : 2023-09-18
+#NBJoursEntrainement = 1000 # soit le dernier jour : 2021-10-08
+
+#print(DernierJour(StartDayEntreainement,NBJoursEntrainement))
+
+
+StartDayValidation = "2023-09-19"
+NBJoursValidation = 200
+NBJoursExperimente = 400
+Experiment_days = [
+	"2024-07-02",
+	#"2025-04-10" ,
+]
+
+
+while 1 :
+	print("")
+	NomModel, epoch = Entrainement(StartDayEntreainement, NBJoursEntrainement, StartDayValidation, NBJoursValidation)
+	Experimente(Experiment_days, NBJoursExperimente, NomModel)
+
+dump_stats()

+ 39 - 0
BourseV4/Model/profil.py

@@ -0,0 +1,39 @@
+import time
+from functools import wraps
+from collections import defaultdict
+
+# Activer/désactiver le profiling directement ici
+ENABLE_PROFILING = False  # False pour prod
+
+# STATS[name] = [count, total_time, max_time]
+STATS = defaultdict(lambda: [0, 0.0, 0.0])
+
+def profile(func):
+	if not ENABLE_PROFILING:
+		return func
+
+	name = f"{func.__module__}.{func.__qualname__}"
+
+	@wraps(func)
+	def wrapper(*args, **kwargs):
+		start = time.perf_counter()
+		STATS[name][0] += 1
+		try:
+			return func(*args, **kwargs)
+		finally:
+			elapsed = time.perf_counter() - start
+			STATS[name][1] += elapsed
+			if elapsed > STATS[name][2]:
+				STATS[name][2] = elapsed
+
+	return wrapper
+
+def dump_stats():
+	print("\n--- PROFILING STATS ---")
+	for name, (count, total, max_t) in sorted(
+		STATS.items(),
+		key=lambda x: x[1][1],
+		reverse=True
+	):
+		avg = total / count if count else 0
+		print(f"{name:60} {count:8} {total:10.4f}s avg={avg:.6f}s max={max_t:.6f}s")