'F7', 'DE' => 'DE' ]; /** * User-Agent pour simuler un navigateur réel et éviter les blocages anti-bot */ const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36 Edg/100.0.1185.36'; // ============================================================================ // FONCTIONS UTILITAIRES // ============================================================================ /** * Récupère et décode les données JSON depuis une URL * * Utilise cURL pour effectuer une requête HTTP GET avec simulation de navigateur * * @param string $url L'URL de l'API à interroger * @return array Les données JSON décodées en tableau associatif * @throws RuntimeException Si la requête échoue ou si le JSON est invalide */ function fetch_json(string $url): array { // Configuration des en-têtes HTTP pour simuler un navigateur réel $headers = [ 'User-Agent: ' . USER_AGENT, 'Referer: https://www.eex.com/', 'Accept-Encoding: gzip, deflate', 'Accept-Language: fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7', 'DNT: 1' // Do Not Track pour respecter la vie privée ]; // Initialisation et configuration de cURL $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, // Retourner la réponse au lieu de l'afficher CURLOPT_ENCODING => '', // Accepter gzip/deflate automatiquement CURLOPT_TIMEOUT => 60, // Timeout de 60 secondes CURLOPT_FAILONERROR => false, // Gérer manuellement les erreurs HTTP ]); // Exécution de la requête HTTP $resp = curl_exec($ch); $err = curl_error($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); // Vérification des erreurs de connexion cURL if ($resp === false) { throw new RuntimeException("Curl error: $err"); } // Vérification du code de statut HTTP (erreurs 4xx et 5xx) if ($httpCode >= 400) { throw new RuntimeException("HTTP error $httpCode while fetching $url"); } // Décodage du JSON $json = json_decode($resp, true); if ($json === null && json_last_error() !== JSON_ERROR_NONE) { throw new RuntimeException('JSON decode error: ' . json_last_error_msg()); } error_log("Données récupérées depuis $url"); return $json; } /** * Convertit une chaîne de date en objet DateTime * * @param string|null $value La chaîne de date (peut être null) * @param string $format Le format de date attendu (par défaut: format américain avec AM/PM) * @return DateTime|null L'objet DateTime ou null si la conversion échoue */ function parse_datetime_nullable(?string $value, string $format = 'm/d/Y g:i:s A'): ?DateTime { if (empty($value)) { return null; } $dt = DateTime::createFromFormat($format, $value); return $dt ?: null; } // ============================================================================ // GESTION DES DONNÉES FUTURES EEX // ============================================================================ /** * Insère les données de contrats futures dans la base de données * * Les contrats futures sont des engagements d'achat/vente à terme * * @param PDO $pdo Connexion à la base de données * @param array $items Tableau des données à insérer * @return void */ function insert_futures_into_MySQL(PDO $pdo, array $items): void { // Requête SQL avec INSERT IGNORE pour éviter les doublons $sql = "INSERT IGNORE INTO eex_futures (pricesymbol, tradedatetimegmt, onexchsingletradevolume, onexchtradevolumeeex, offexchtradevolumeeex, openinterest, ontradeprice, close) VALUES (:pricesymbol, :tradedatetimegmt, :onexchsingletradevolume, :onexchtradevolumeeex, :offexchtradevolumeeex, :openinterest, :ontradeprice, :close)"; $stmt = $pdo->prepare($sql); $pdo->beginTransaction(); // Transaction pour améliorer les performances $count = 0; foreach ($items as $item) { // Ignorer les lignes sans prix de clôture (données incomplètes) if (!isset($item['close']) || $item['close'] === '') { continue; } // Extraction du symbole de prix $pricesymbol = $item['gv.pricesymbol'] ?? null; // Conversion de la date ou utilisation de la date actuelle par défaut $dt = parse_datetime_nullable($item['tradedatetimegmt']); if ($dt === null) { $dt = new DateTime(); } // Préparation des paramètres pour l'insertion $params = [ ':pricesymbol' => $pricesymbol, ':tradedatetimegmt' => $dt->format('Y-m-d H:i:s'), ':onexchsingletradevolume' => $item['onexchsingletradevolume'] ?? null, // Volume single trade ':onexchtradevolumeeex' => $item['onexchtradevolumeeex'] ?? null, // Volume total échangé ':offexchtradevolumeeex' => $item['offexchtradevolumeeex'] ?? null, // Volume hors plateforme ':openinterest' => $item['openinterest'] ?? null, // Positions ouvertes ':ontradeprice' => isset($item['ontradeprice']) ? (string)$item['ontradeprice'] : null, ':close' => (string)$item['close'], // Prix de clôture ]; try { $stmt->execute($params); $count += $stmt->rowCount(); } catch (Throwable $e) { error_log('Erreur format/insertion futures: ' . $e->getMessage()); } } $pdo->commit(); error_log("Inséré $count lignes dans eex_futures"); } /** * Construit l'URL de l'API EEX pour récupérer les données futures * * @param string $optionRoot Le code du produit (ex: "/E.F7PY" pour France Peak Year) * @return string L'URL complète de l'API */ function url_eex_futures(string $optionRoot): string { // Utilisation de J-1 et J-2 pour récupérer les dernières données disponibles $dayMinusOne = (new DateTime('-1 day'))->format('Y/m/d'); $dayMinusTwo = (new DateTime('-2 days'))->format('Y/m/d'); $params = [ 'expirationdate' => $dayMinusTwo, 'onDate' => $dayMinusOne, 'optionroot' => $optionRoot, ]; // Construction de l'URL avec les champs à récupérer $base = 'https://webservice-eex.gvsi.com/query/json/getChain/gv.pricesymbol/gv.displaydate/gv.expirationdate/tradedatetimegmt/gv.eexdeliverystart/ontradeprice/close/onexchsingletradevolume/onexchtradevolumeeex/offexchtradevolumeeex/openinterest/'; return $base . '?' . http_build_query($params); } /** * Récupère les données futures depuis l'API et les insère dans la base * * @param string $url L'URL de l'API à interroger * @param PDO $pdo Connexion à la base de données * @return void */ function get_futures_data_and_insert_in_db(string $url, PDO $pdo): void { try { $json = fetch_json($url); if (!empty($json['results']['items']) && is_array($json['results']['items'])) { insert_futures_into_MySQL($pdo, $json['results']['items']); } else { error_log('Aucun item trouvé dans la réponse futures'); } } catch (Throwable $e) { error_log('Erreur récupération/insertion futures: ' . $e->getMessage()); } } /** * Récupère tous les contrats futures d'électricité (France et Allemagne) * * Parcourt toutes les combinaisons : * - Pays : FR, DE * - Type : Peak (P), BaseLoad (B) * - Période : Year (Y), Quarter (Q), Month (M) * * @param PDO $pdo Connexion à la base de données * @return void */ function get_and_insert_power_futures(PDO $pdo): void { $dataType = ['Peak' => 'P', 'BaseLoad' => 'B']; // P = heures de pointe, B = base $periods = ['Year' => 'Y', 'Quarter' => 'Q', 'Month' => 'M']; // Échéances foreach (COUNTRY_CODES as $country => $code) { foreach ($dataType as $tVal) { foreach ($periods as $pVal) { $startTime = microtime(true); // Construction du code produit (ex: "/E.F7PY" pour France Peak Year) $optionRoot = "\"/E.{$code}{$tVal}{$pVal}\""; error_log("Start : récupération pour {$country} {$tVal} {$pVal} -> {$optionRoot}"); // Délai aléatoire entre 5 et 10 secondes pour éviter la surcharge serveur usleep(mt_rand(5_000_000, 10_000_000)); get_futures_data_and_insert_in_db(url_eex_futures($optionRoot), $pdo); $duration = microtime(true) - $startTime; error_log("Données traitées pour {$optionRoot} en " . round($duration, 2) . "s"); } } } } /** * Récupère les contrats futures EUA (European Union Allowances - quotas carbone) * * @param PDO $pdo Connexion à la base de données * @return void */ function get_and_insert_eua_futures(PDO $pdo): void { $optionRoot = '"/E.FEUA"'; // Code pour les quotas carbone européens get_futures_data_and_insert_in_db(url_eex_futures($optionRoot), $pdo); error_log("Données traitées pour $optionRoot"); } /** * Récupère les contrats futures gaz naturel * * @param PDO $pdo Connexion à la base de données * @return void */ function get_and_insert_natural_gaz_futures(PDO $pdo): void { $periods = ['Year' => 'Y', 'Quarter' => 'Q', 'Month' => 'M']; foreach ($periods as $pVal) { // Délai aléatoire entre 20 et 90 secondes pour respecter les limites de l'API usleep(mt_rand(20_000_000, 90_000_000)); $optionRoot = "\"/E.G5B{$pVal}\""; // G5B = Gaz naturel baseload get_futures_data_and_insert_in_db(url_eex_futures($optionRoot), $pdo); error_log("Données traitées pour $optionRoot"); } } // ============================================================================ // GESTION DES DONNÉES SPOT EEX (GAZ NATUREL) // ============================================================================ /** * Insère les données de prix spot (marché au jour le jour) dans la base * * @param PDO $pdo Connexion à la base de données * @param array $items Tableau des données à insérer * @param string $symbol Symbole du produit * @return void */ function insert_spot_into_MySQL(PDO $pdo, array $items, string $symbol): void { $sql = "INSERT IGNORE INTO eex_spot (pricesymbol, tradedatetimegmt, onexchtradevolumeeex, onexchsingletradevolume, ontradeprice, close) VALUES (:pricesymbol, :tradedatetimegmt, :onexchtradevolumeeex, :onexchsingletradevolume, :ontradeprice, :close)"; $stmt = $pdo->prepare($sql); $pdo->beginTransaction(); $count = 0; foreach ($items as $item) { // Conversion de la date $dt = parse_datetime_nullable($item['tradedatetimegmt']); if ($dt === null) { continue; // Ignorer les lignes sans date valide } $params = [ ':pricesymbol' => $symbol, ':tradedatetimegmt' => $dt->format('Y-m-d H:i:s'), ':onexchtradevolumeeex' => $item['onexchtradevolumeeex'] ?? null, ':onexchsingletradevolume' => $item['onexchsingletradevolume'] ?? null, ':ontradeprice' => isset($item['ontradeprice']) ? (string)$item['ontradeprice'] : null, ':close' => isset($item['close']) ? (string)$item['close'] : null, ]; try { $stmt->execute($params); $count += $stmt->rowCount(); } catch (Throwable $e) { error_log('Erreur insertion spot: ' . $e->getMessage()); } } $pdo->commit(); error_log("Inserted $count rows into eex_spot"); } /** * Construit l'URL de l'API EEX pour récupérer les données spot * * @param string $priceSymbol Le symbole du produit * @return string L'URL complète de l'API */ function url_eex_spot(string $priceSymbol): string { $dayMinusOne = (new DateTime('-1 day'))->format('Y/m/d'); $params = [ 'chartstartdate' => $dayMinusOne, 'chartstopdate' => $dayMinusOne, 'dailybarinterval' => 'Days', 'aggregatepriceselection' => 'First', 'priceSymbol' => $priceSymbol, ]; $base = 'https://webservice-eex.gvsi.com/query/json/getDaily/ontradeprice/onexchsingletradevolume/close/onexchtradevolumeeex/tradedatetimegmt/'; return $base . '?' . http_build_query($params); } /** * Récupère les données spot depuis l'API et les insère dans la base * * @param string $url L'URL de l'API * @param PDO $pdo Connexion à la base de données * @param string $symbol Symbole du produit * @return void */ function get_spot_data_and_insert_in_db(string $url, PDO $pdo, string $symbol): void { try { $json = fetch_json($url); if (!empty($json['results']['items']) && is_array($json['results']['items'])) { insert_spot_into_MySQL($pdo, $json['results']['items'], $symbol); error_log('Données SPOT insérées avec succès'); } else { error_log('Aucun item SPOT trouvé'); } } catch (Throwable $e) { error_log('Erreur récupération/insertion spot: ' . $e->getMessage()); } } /** * Récupère les prix spot gaz naturel pour tous les hubs européens * * Hubs couverts : * - CEGH (Autriche) * - OTE (République Tchèque) * - ETF (Pays-Bas - anciennement) * - NBP (Royaume-Uni) * - PEG (France - ancien hub) * - PVB (Espagne) * - THE (Allemagne - THE) * - TTF (Pays-Bas - principal hub européen) * - ZTP (Belgique) * * Chaque hub a des prix Day-Ahead (GND/GPND/GTND) et Weekend (GWE/GPWE/GSWE/GTWE) * * @param PDO $pdo Connexion à la base de données * @return void */ function get_and_insert_natural_gaz_spot(PDO $pdo): void { $symbols = [ '#E.CEGH_GND1','#E.CEGH_GWE1', // CEGH Autriche '#E.OTE_GSND','#E.OTE_GSWE', // OTE République Tchèque '#E.ETF_GND1', '#E.ETF_GWE1', // ETF Pays-Bas '#E.NBP_GPND', '#E.NBP_GPWE', // NBP Royaume-Uni '#E.PEG_GND1', '#E.PEG_GWE1', // PEG France '#E.PVB_GSND', '#E.PVB_GSWE', // PVB Espagne '#E.THE_GND1','#E.THE_GWE1', // THE Allemagne '#E.TTF_GND1', '#E.TTF_GWE1', // TTF Pays-Bas (hub principal) '#E.ZTP_GTND', '#E.ZTP_GTWE' // ZTP Belgique ]; foreach ($symbols as $s) { // Délai aléatoire important pour ne pas surcharger l'API usleep(mt_rand(20_000_000, 90_000_000)); $currentSymbol = "\"{$s}\""; get_spot_data_and_insert_in_db(url_eex_spot($currentSymbol), $pdo, $currentSymbol); error_log("Données traitées pour le code $s"); } } // ============================================================================ // GESTION DES DONNÉES SPOT ÉLECTRICITÉ (NORDPOOL) // ============================================================================ /** * Insère les prix spot électricité France (données horaires Day-Ahead) * * @param PDO $pdo Connexion à la base de données * @param array $data Données JSON de l'API Nordpool * @return void */ function insert_power_spot_into_MySQL(PDO $pdo, array $data): void { // Vérification de la structure des données if (empty($data['multiAreaEntries']) || !is_array($data['multiAreaEntries'])) { error_log('Structure power spot inattendue'); return; } foreach ($data['multiAreaEntries'] as $item) { try { // Extraction des métadonnées globales $deliveryAreas = $data['deliveryAreas'][0] ?? ''; $deliveryDateCET = isset($data['deliveryDateCET']) ? (new DateTime($data['deliveryDateCET']))->format('Y-m-d H:i:s') : null; $market = $data['market'] ?? null; // Extraction des données horaires $deliveryStart = isset($item['deliveryStart']) ? (new DateTime(str_replace('Z', '+00:00', $item['deliveryStart'])))->format('Y-m-d H:i:s') : null; $deliveryEnd = isset($item['deliveryEnd']) ? (new DateTime(str_replace('Z', '+00:00', $item['deliveryEnd'])))->format('Y-m-d H:i:s') : null; $entryFR = isset($item['entryPerArea']['FR']) ? (string)$item['entryPerArea']['FR'] : null; // Vérification manuelle des doublons (car structure complexe) $checkSql = "SELECT 1 FROM eex_power_spot_fr WHERE deliveryAreas = :deliveryAreas AND deliveryStart = :deliveryStart AND deliveryEnd = :deliveryEnd LIMIT 1"; $checkStmt = $pdo->prepare($checkSql); $checkStmt->execute([ ':deliveryAreas' => $deliveryAreas, ':deliveryStart' => $deliveryStart, ':deliveryEnd' => $deliveryEnd, ]); $exists = $checkStmt->fetchColumn(); // Insertion uniquement si pas de doublon if (!$exists) { $insertSql = "INSERT IGNORE INTO eex_power_spot_fr (deliveryAreas, deliveryDateCET, market, deliveryStart, deliveryEnd, entryFR) VALUES (:deliveryAreas, :deliveryDateCET, :market, :deliveryStart, :deliveryEnd, :entryFR)"; $insertStmt = $pdo->prepare($insertSql); $insertStmt->execute([ ':deliveryAreas' => $deliveryAreas, ':deliveryDateCET' => $deliveryDateCET, ':market' => $market, ':deliveryStart' => $deliveryStart, ':deliveryEnd' => $deliveryEnd, ':entryFR' => $entryFR, ]); error_log("Inserted power spot: $deliveryAreas $deliveryStart - $deliveryEnd"); } else { error_log("Skipped duplicate power spot: $deliveryAreas $deliveryStart - $deliveryEnd"); } } catch (Throwable $e) { error_log('Erreur insertion power spot: ' . $e->getMessage()); } } } /** * Récupère les prix spot électricité Day-Ahead pour la France depuis Nordpool * * @param PDO $pdo Connexion à la base de données * @return void */ function get_power_spot_fr(PDO $pdo): void { $base = 'https://dataportal-api.nordpoolgroup.com/api/DayAheadPrices'; $date = (new DateTime('-1 day'))->format('Y/m/d'); $params = [ 'date' => $date, 'market' => 'DayAhead', // Marché Day-Ahead (J+1) 'deliveryArea' => 'FR', // Zone France 'currency' => 'EUR' // Prix en euros ]; $url = $base . '?' . http_build_query($params); try { $json = fetch_json($url); insert_power_spot_into_MySQL($pdo, $json); } catch (Throwable $e) { error_log('Erreur get_power_spot_fr: ' . $e->getMessage()); } } // ============================================================================ // GESTION DE LA BASE DE DONNÉES // ============================================================================ /** * Crée les tables MySQL si elles n'existent pas * * Tables créées : * - eex_power_spot_fr : Prix spot électricité France (Nordpool) * - eex_spot : Prix spot gaz naturel (EEX) * - eex_futures : Contrats futures (électricité, gaz, EUA) * * @param PDO $pdo Connexion à la base de données * @return void */ function createTablesInexistantes(PDO $pdo): void { $sqls = [ // Table pour les prix spot électricité France (données horaires) "CREATE TABLE IF NOT EXISTS eex_power_spot_fr ( deliveryAreas VARCHAR(255), -- Zone de livraison (ex: FR) deliveryDateCET DATETIME, -- Date de livraison (CET) market VARCHAR(255), -- Type de marché (DayAhead, Intraday...) deliveryStart DATETIME, -- Début de la période horaire deliveryEnd DATETIME, -- Fin de la période horaire entryFR DECIMAL(18,4), -- Prix en EUR/MWh PRIMARY KEY (deliveryAreas, deliveryStart, deliveryEnd) ) ENGINE=InnoDB;", // Table pour les prix spot gaz naturel "CREATE TABLE IF NOT EXISTS eex_spot ( pricesymbol VARCHAR(255), -- Symbole du produit (ex: #E.TTF_GND1) tradedatetimegmt DATETIME, -- Date/heure de transaction (GMT) onexchtradevolumeeex DECIMAL, -- Volume échangé sur plateforme onexchsingletradevolume DECIMAL, -- Volume single trade ontradeprice DECIMAL(18,4), -- Prix de transaction close DECIMAL(18,4), -- Prix de clôture PRIMARY KEY (pricesymbol, tradedatetimegmt) ) ENGINE=InnoDB;", // Table pour les contrats futures (électricité, gaz, EUA) "CREATE TABLE IF NOT EXISTS eex_futures ( pricesymbol VARCHAR(255), -- Symbole du contrat (ex: /E.F7PY) tradedatetimegmt DATETIME, -- Date/heure de transaction (GMT) onexchsingletradevolume DECIMAL, -- Volume single trade onexchtradevolumeeex DECIMAL, -- Volume total échangé offexchtradevolumeeex DECIMAL, -- Volume hors plateforme (OTC) openinterest DECIMAL, -- Positions ouvertes (intérêt ouvert) ontradeprice DECIMAL(18,4), -- Prix de transaction close DECIMAL(18,4), -- Prix de clôture PRIMARY KEY (pricesymbol, tradedatetimegmt, close) ) ENGINE=InnoDB;" ]; foreach ($sqls as $s) { $pdo->exec($s); } error_log('Tables vérifiées / créées si nécessaire'); } // ============================================================================ // FONCTION PRINCIPALE // ============================================================================ /** * Point d'entrée principal du script * * Étapes : * 1. Chargement de la configuration BDD * 2. Connexion à MySQL * 3. Création des tables * 4. Scraping de toutes les données (futures + spot) * * @return void */ function main(): void { // Chargement du fichier de configuration $configPath = __DIR__ . '/config.ini'; if (!file_exists($configPath)) { throw new RuntimeException("Fichier de configuration introuvable: $configPath"); } $config = parse_ini_file($configPath, true); if (!isset($config['APPROS_EEX_DATABASE'])) { throw new RuntimeException("Section 'APPROS_EEX_DATABASE' manquante dans config.ini"); } // Extraction des paramètres de connexion $db = $config['APPROS_EEX_DATABASE']; $host = $db['HOST'] ?? '172.16.1.4'; $port = $db['PORT'] ?? '3306'; $user = $db['USER'] ?? 'user_odbc'; $pass = $db['PASSWORD'] ?? 'My@ppro!2023$'; $name = $db['DATABASE'] ?? 'appros'; // Connexion à MySQL avec gestion d'erreur try { $dsn = "mysql:host=$host;port=$port;dbname=$name;charset=utf8mb4"; $pdo = new PDO($dsn, $user, $pass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]); error_log('Connexion MySQL réussie.'); } catch (PDOException $e) { error_log('Échec connexion MySQL: ' . $e->getMessage()); return; } // Création des tables si nécessaire createTablesInexistantes($pdo); // ======================================================================== // SCRAPING DE TOUTES LES DONNÉES // ======================================================================== // 1. Futures électricité (France + Allemagne, Peak + Base, Year + Quarter + Month) get_and_insert_power_futures($pdo); // 2. Futures quotas carbone EUA get_and_insert_eua_futures($pdo); // 3. Futures gaz naturel (Year + Quarter + Month) get_and_insert_natural_gaz_futures($pdo); // 4. Spot gaz naturel (tous les hubs européens) get_and_insert_natural_gaz_spot($pdo); // 5. Spot électricité France (prix horaires Day-Ahead) get_power_spot_fr($pdo); // Fermeture de la connexion $pdo = null; error_log('Fin du programme de scraping EEX.'); } // ============================================================================ // EXÉCUTION DU SCRIPT (CLI uniquement) // ============================================================================ // Vérification que le script est exécuté en ligne de commande if (php_sapi_name() === 'cli') { try { main(); } catch (Throwable $e) { error_log('Erreur fatale: ' . $e->getMessage()); echo 'Erreur fatale: ' . $e->getMessage() . PHP_EOL; exit(1); } }