On charge les données SPY pour la période d’entraînement (2015-2022) et de test (2022-2026).
# Ajouter SPYsymbol = qb.add_equity("SPY", Resolution.DAILY).symbol# Charger l'historique (2015-2026)start = datetime(2015, 1, 1)end = datetime(2026, 1, 1)history = qb.history(symbol, start, end, Resolution.DAILY)print(f"Donnees chargees: {len(history)} lignes")# Handle MultiIndex from qb.history (Symbol, Time) -> extract time for displayifisinstance(history.index, pd.MultiIndex): dates = history.index.get_level_values(-1)print(f"Periode: {dates[0].date()} a {dates[-1].date()}")else:print(f"Periode: {history.index[0].date()} a {history.index[-1].date()}")
Donnees chargees: 2766 lignes
Periode: 2015-01-02 a 2025-12-31
Extraction de la série de prix de clôture et des dates à partir de l’historique BTCUSD pour alimenter le modèle LSTM.
# Extraire les prix de clôtureprices = history['close'].valuesdates = history.index# Fail-fast guard (C941-L/C942-L, #8734, ai-01 c.38): presence != freshness.# add_equity() does not raise on an absent symbol; fillDataForward=True (default)# extends a truncated zip (e.g. ending 2021-03-31) as a CONSTANT. Assert on the# LOADED history (no reliable fillDataForward setter, C943-L). SPY is provisioned# by provision_lean_data.py (universe turn_of_month/ema_cross_alpha/fama_french).iflen(prices) ==0:raiseValueError("SPY loaded no daily bars -- regenerate via ""provision_lean_data.py (C941-L).")# C935-L: qb.history() returns a (Symbol, Time) MultiIndex, so dates[-1] is a# TUPLE (Symbol, Timestamp) -- getattr(tuple, 'year') would be 0 and false-fire.# Extract the TIME level before checking the last bar's year._times = dates ifnotisinstance(dates, pd.MultiIndex) else dates.get_level_values(-1)_last = _times[-1]ifgetattr(_last, 'year', 0) <2024:raiseValueError(f"SPY data ends {_last} -- Lean fillDataForward=True "f"would extend it as a CONSTANT (C942-L, #8734). "f"Regenerate the fresh zip before trusting post-threshold metrics.")print(f"Prix: {len(prices)} jours de trading")print(f"Premier prix: ${prices[0]:.2f}")print(f"Dernier prix: ${prices[-1]:.2f}")print(f"Prix min: ${prices.min():.2f}")print(f"Prix max: ${prices.max():.2f}")
Prix: 2766 jours de trading
Premier prix: $169.64
Dernier prix: $678.32
Prix min: $154.29
Prix max: $686.73
2. Préparation des données
Normalisation min-max et création des séquences temporelles.
class PriceDataset(Dataset):"""Dataset pour les séquences de prix."""def__init__(self, prices, seq_length=20):self.prices = pricesself.seq_length = seq_lengthdef__len__(self):returnlen(self.prices) -self.seq_lengthdef__getitem__(self, idx):# Séquence d'entrée: seq_length prix x =self.prices[idx:idx +self.seq_length]# Cible: prix suivant y =self.prices[idx +self.seq_length]return torch.FloatTensor(x), torch.FloatTensor([y])def normalize_prices(prices, train_size=0.8):"""Normalise les prix avec min-max scaling.""" split_idx =int(len(prices) * train_size) train_prices = prices[:split_idx] scaler_min = train_prices.min() scaler_max = train_prices.max() normalized = (prices - scaler_min) / (scaler_max - scaler_min)return normalized, scaler_min, scaler_maxdef denormalize(normalized, scaler_min, scaler_max):"""Dénormalise les prix."""return normalized * (scaler_max - scaler_min) + scaler_minprint("Fonctions de préparation définies.")
Fonctions de préparation définies.
Traitement des données et calcul des variables intermédiaires pour la stratégie DL-LSTM (étape 8).
# ParamètresSEQ_LENGTH =20TRAIN_SIZE =0.8BATCH_SIZE =32# Normalisationnormalized_prices, scaler_min, scaler_max = normalize_prices(prices, TRAIN_SIZE)data_sha256 = hashlib.sha256(np.asarray(prices).tobytes()).hexdigest()# Split train/testsplit_idx =int(len(normalized_prices) * TRAIN_SIZE)train_prices = normalized_prices[:split_idx]test_prices = normalized_prices[split_idx:]# Créer les datasetstrain_dataset = PriceDataset(train_prices, SEQ_LENGTH)test_dataset = PriceDataset(test_prices, SEQ_LENGTH)# Le générateur explicite gouverne l'ordre de shuffle indépendamment du RNG global.train_generator = torch.Generator()train_generator.manual_seed(SEED)# Créer les dataloaderstrain_loader = DataLoader( train_dataset, batch_size=BATCH_SIZE, shuffle=True, generator=train_generator,)test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)print(f"Train samples: {len(train_dataset)}")print(f"Test samples: {len(test_dataset)}")print(f"Scaler range: [{scaler_min:.2f}, {scaler_max:.2f}]")print(f"Empreinte des données: {data_sha256}")print(f"Ordre du DataLoader d'entraînement seedé avec seed={SEED}.")
Train samples: 2192
Test samples: 534
Scaler range: [154.29, 449.49]
Empreinte des données: 897a5cb445127e6b1af3b69799235cd56da745fb45c4ddac624d37041d732f49
Ordre du DataLoader d'entraînement seedé avec seed=42.
Interprétation: Préparation des données
Normalisation: Les prix sont mis à l’échelle [0, 1] pour stabiliser l’entraînement
Séquences: Chaque exemple contient 20 prix consécutifs pour prédire le 21ème
Split: 80% train (2015-2022), 20% test (2022-2026)
3. Architecture LSTM
class LSTMModel(nn.Module):"""Modèle LSTM pour la prédiction de prix."""def__init__(self, hidden_size=50, num_layers=2):super(LSTMModel, self).__init__()self.hidden_size = hidden_sizeself.num_layers = num_layers# LSTM bidirectionnelself.lstm = nn.LSTM( input_size=1, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, bidirectional=True )# Couches fully connectedself.fc1 = nn.Linear(hidden_size *2, 32) # *2 car bidirectionnelself.relu = nn.ReLU()self.fc2 = nn.Linear(32, 1)def forward(self, x):# x shape: (batch, seq_length, 1) lstm_out, _ =self.lstm(x)# Prendre le dernier output last_output = lstm_out[:, -1, :]# Passer par les couches FC x =self.fc1(last_output) x =self.relu(x) x =self.fc2(x)return x# Créer le modèlemodel = LSTMModel(hidden_size=50, num_layers=2)# Déplacer sur GPU si disponibledevice = torch.device('cuda'if torch.cuda.is_available() else'cpu')model = model.to(device)print(f"Modèle LSTM créé.")print(f"Device: {device}")print(f"Paramètres: {sum(p.numel() for p in model.parameters()):,}")