From: Valentin Hulin Date: Tue, 24 Feb 2026 07:49:00 +0000 (+0100) Subject: Ajout du dossier Val_Crypto X-Git-Url: https://git.digitality.be/?a=commitdiff_plain;h=f82ec968c12f0ebc7c9a20adf5053b56f6cec8c2;p=pdw25-26 Ajout du dossier Val_Crypto --- diff --git a/server/Val_Crypto/app.js b/server/Val_Crypto/app.js new file mode 100644 index 0000000..4bb257a --- /dev/null +++ b/server/Val_Crypto/app.js @@ -0,0 +1 @@ +import "./server.js"; diff --git a/server/Val_Crypto/db.js b/server/Val_Crypto/db.js new file mode 100644 index 0000000..ee443a3 --- /dev/null +++ b/server/Val_Crypto/db.js @@ -0,0 +1,9 @@ +import mysql from "mysql2/promise"; + +export const db = mysql.createPool({ + host: "localhost", + user: "root", + password: "vava", + database: "crypto", + port: 3306 +}); diff --git a/server/Val_Crypto/public/index.html b/server/Val_Crypto/public/index.html new file mode 100644 index 0000000..b021b5f --- /dev/null +++ b/server/Val_Crypto/public/index.html @@ -0,0 +1,109 @@ + + + + + + Val Crypto - Prix + + + +

Val Crypto - Prix (toutes les 5 min)

+ +
+ + — +
+ +
+ +

+ API : + /api/price/current?pair=BTC/EUR | + /api/price/current?pair=BTC/USDT +

+ + + + \ No newline at end of file diff --git a/server/Val_Crypto/repositories/pair.repository.js b/server/Val_Crypto/repositories/pair.repository.js new file mode 100644 index 0000000..484251b --- /dev/null +++ b/server/Val_Crypto/repositories/pair.repository.js @@ -0,0 +1,75 @@ +import { db } from "../db.js"; + +/** + * Accepte: + * - BTC/EUR, BTC_USDT, BTC-USDT, "btc eur" + * Et match la DB qui stocke pair_code = BTC_EUR etc. + */ +export async function getActivePairIdByCode(pairCode) { + const raw = String(pairCode || "").trim().toUpperCase(); + const normalized = raw.replace(/\s+/g, "").replace(/[-/]/g, "_"); + + const [rows] = await db.execute( + ` + SELECT pair_id + FROM pairs + WHERE pair_code = ? + LIMIT 1 + `, + [normalized] + ); + + if (rows.length === 0) { + const err = new Error(`PAIR_NOT_FOUND: ${normalized}`); + err.code = "PAIR_NOT_FOUND"; + throw err; + } + + return rows[0].pair_id; +} + +/** + * Récupère une paire par son ID. + * On récupère pair_code pour reconstruire base/quote sans dépendre de base_symbol/quote_symbol. + */ +export async function getPairById(pairId) { + const id = Number(pairId); + + const [rows] = await db.execute( + ` + SELECT pair_id, base_symbol, quote_symbol, pair_code + FROM pairs + WHERE pair_id = ? + LIMIT 1 + `, + [id] + ); + + if (rows.length === 0) { + const err = new Error(`PAIR_ID_NOT_FOUND: ${id}`); + err.code = "PAIR_ID_NOT_FOUND"; + throw err; + } + + return rows[0]; +} +export async function listActivePairs() { + const [rows] = await db.execute(` + SELECT pair_id, base_symbol, quote_symbol, pair_code, is_active + FROM pairs + WHERE is_active = 1 + ORDER BY pair_id ASC + `); + return rows; +} + +export async function listAllPairs() { + const [rows] = await db.execute( + ` + SELECT pair_id, base_symbol, quote_symbol, pair_code, is_active + FROM pairs + ORDER BY pair_id ASC + ` + ); + return rows; +} \ No newline at end of file diff --git a/server/Val_Crypto/repositories/price.repository.js b/server/Val_Crypto/repositories/price.repository.js new file mode 100644 index 0000000..6dec8cb --- /dev/null +++ b/server/Val_Crypto/repositories/price.repository.js @@ -0,0 +1,201 @@ +import { db } from "../db.js"; +import crypto from "crypto"; + +/** + * Insère / met à jour une BOUGIE OHLC (5 minutes) dans la table existante. + * - interval_sec = 300 + * - timestamp_ms = début du bucket (arrondi) + * - open = premier prix du bucket + * - high/low = extrêmes du bucket + * - close = dernier prix du bucket + * + * ⚠️ Pour que high/low varient, il faut appeler insertPricePoint + * plus souvent que toutes les 5 minutes (ex: toutes les 10–30s). + */ +export async function insertPricePoint({ + pair_id, + timestamp_ms, + current_price, + source, + volume_24h = null, + candle_close = null +}) { + const pid = Number(pair_id); + const ts = Number(timestamp_ms); + const price = Number(current_price); + const src = String(source || "").toUpperCase().trim(); + + if (!Number.isFinite(pid)) throw new Error("pair_id invalide"); + if (!Number.isFinite(ts)) throw new Error("timestamp_ms invalide"); + if (!Number.isFinite(price) || price <= 0) throw new Error("current_price invalide (>0)"); + if (!src) throw new Error("source obligatoire"); + + const intervalSec = 300; // 5 minutes + + // ✅ bucket start (début de la bougie) + const bucketStart = Math.floor(ts / (intervalSec * 1000)) * (intervalSec * 1000); + const bucketEnd = bucketStart + (intervalSec * 1000) - 1; + + const closeValue = candle_close != null ? Number(candle_close) : price; + if (!Number.isFinite(closeValue) || closeValue <= 0) { + throw new Error("candle_close invalide (>0)"); + } + + const vol = volume_24h == null ? null : Number(volume_24h); + if (vol != null && (!Number.isFinite(vol) || vol < 0)) { + throw new Error("volume invalide (>=0)"); + } + + // ✅ Cherche une bougie existante DANS le bucket (même si timestamp_ms n'est pas arrondi) + const [existing] = await db.execute( + ` + SELECT price_id, open_price, high_price, low_price, close_price, timestamp_ms + FROM price_points + WHERE pair_id = ? + AND source = ? + AND interval_sec = ? + AND timestamp_ms BETWEEN ? AND ? + ORDER BY timestamp_ms ASC + LIMIT 1 + `, + [pid, src, intervalSec, bucketStart, bucketEnd] + ); + + // 2) Si pas de bougie -> on crée une nouvelle + if (existing.length === 0) { + const priceId = crypto.randomUUID(); + + await db.execute( + ` + INSERT INTO price_points ( + price_id, + pair_id, + source, + interval_sec, + timestamp_ms, + open_price, + high_price, + low_price, + close_price, + volume, + created_at_ms + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, + [ + priceId, + pid, + src, + intervalSec, + bucketStart, + price, // open + price, // high + price, // low + closeValue, // close + vol, + Date.now() + ] + ); + return; + } + + // 3) Sinon -> update OHLC (open ne change pas) + const row = existing[0]; + const prevHigh = Number(row.high_price); + const prevLow = Number(row.low_price); + + const newHigh = Number.isFinite(prevHigh) ? Math.max(prevHigh, price) : price; + const newLow = Number.isFinite(prevLow) ? Math.min(prevLow, price) : price; + + await db.execute( + ` + UPDATE price_points + SET timestamp_ms = ?, -- ✅ on "recalle" la bougie sur le bucketStart + high_price = ?, + low_price = ?, + close_price = ?, + volume = ?, + created_at_ms = ? + WHERE price_id = ? + `, + [bucketStart, newHigh, newLow, closeValue, vol, Date.now(), row.price_id] + ); +} + +/** + * Prix courant = dernière bougie (close_price) + */ +export async function getCurrentPrice(pair_id) { + const pid = Number(pair_id); + + const [rows] = await db.execute( + ` + SELECT + source, + timestamp_ms, + open_price, + high_price, + low_price, + close_price, + volume + FROM price_points + WHERE pair_id = ? + ORDER BY timestamp_ms DESC, created_at_ms DESC + LIMIT 1 + `, + [pid] + ); + + const r = rows[0]; + if (!r) return null; + + return { + source: r.source, + timestamp_ms: r.timestamp_ms, + current_price: r.close_price, + open_price: r.open_price, + high_price: r.high_price, + low_price: r.low_price, + close_price: r.close_price, + volume_24h: r.volume ?? null, + candle_close: r.close_price + }; +} + +/** + * Historique = dernières bougies (OHLC) + */ +export async function getPriceHistory(pair_id, limit = 200) { + const pid = Number(pair_id); + const lim = Math.max(1, Math.min(2000, Number(limit) || 200)); + + const [rows] = await db.execute( + ` + SELECT + source, + timestamp_ms, + open_price, + high_price, + low_price, + close_price, + volume + FROM price_points + WHERE pair_id = ? + ORDER BY timestamp_ms DESC, created_at_ms DESC + LIMIT ? + `, + [pid, lim] + ); + + return rows.reverse().map((r) => ({ + source: r.source, + timestamp_ms: r.timestamp_ms, + current_price: r.close_price, + open_price: r.open_price, + high_price: r.high_price, + low_price: r.low_price, + close_price: r.close_price, + volume_24h: r.volume ?? null, + candle_close: r.close_price + })); +} \ No newline at end of file diff --git a/server/Val_Crypto/server.js b/server/Val_Crypto/server.js new file mode 100644 index 0000000..f652eec --- /dev/null +++ b/server/Val_Crypto/server.js @@ -0,0 +1,286 @@ +import express from "express"; +import { db } from "./db.js"; + +import { formatProviderError, getPriceWithFallback } from "./services/price.service.js"; +import { getActivePairIdByCode, getPairById, listActivePairs } from "./repositories/pair.repository.js"; +import { insertPricePoint, getCurrentPrice, getPriceHistory } from "./repositories/price.repository.js"; + +const app = express(); +const PORT = 3000; + +// On fetch souvent pour construire OHLC (bougie 5 min) +const SAMPLE_INTERVAL_MS = 10 * 1000; // 10s + +// Toutes les paires actives (chargées au démarrage) +let PAIR_IDS_TO_SAMPLE = []; + +app.use(express.json()); +app.use(express.static("public")); + +function ok(res, data) { + return res.json({ ok: true, data }); +} + +function fail(res, status, code, message, details = null) { + return res.status(status).json({ + ok: false, + error: { code, message, details } + }); +} + +function normalizePairCode(value) { + return String(value || "").trim().toUpperCase(); +} + +/** + * Convertit: + * - BTC/EUR, BTC_EUR, BTC-EUR, "BTC EUR" + * en { base:"BTC", quote:"EUR", dbCode:"BTC_EUR" } + */ +function parsePairInput(pairInput) { + const raw = normalizePairCode(pairInput); + const dbCode = raw.replace(/\s+/g, "").replace(/[-/]/g, "_"); + const parts = dbCode.split("_"); + if (parts.length !== 2 || !parts[0] || !parts[1]) { + const err = new Error("PAIR_INVALID"); + err.code = "PAIR_INVALID"; + throw err; + } + return { base: parts[0], quote: parts[1], dbCode }; +} + +/** + * Reconstruit base/quote depuis pair_code DB (ex: BTC_EUR) + * -> { base:"BTC", quote:"EUR" } + */ +function parsePairCodeFromDb(pairCodeDb) { + const code = String(pairCodeDb || "").trim().toUpperCase(); + const parts = code.split("_"); + if (parts.length !== 2) throw new Error(`PAIR_CODE_DB_INVALID: ${code}`); + return { base: parts[0], quote: parts[1] }; +} + +/** + * Charge toutes les paires actives (is_active=1) depuis la DB + */ +async function loadAllActivePairIds() { + const pairs = await listActivePairs(); // doit être ORDER BY pair_id ASC dans le repo + PAIR_IDS_TO_SAMPLE = pairs.map((p) => p.pair_id); + console.log("PAIR_IDS_TO_SAMPLE (actives):", PAIR_IDS_TO_SAMPLE); +} + +/** + * ✅ Liste des paires (pour ton interface) + * GET /api/pairs + */ +app.get("/api/pairs", async (req, res) => { + try { + const pairs = await listActivePairs(); + return ok(res, { count: pairs.length, pairs }); + } catch (e) { + return fail(res, 500, "DB_ERROR", "Erreur lecture des paires", e.message); + } +}); + +/** + * DEBUG: Voir les paires en DB (tout) + * GET /api/debug/pairs + */ +app.get("/api/debug/pairs", async (req, res) => { + try { + const [rows] = await db.execute( + "SELECT pair_id, base_symbol, quote_symbol, pair_code, is_active FROM pairs ORDER BY pair_id" + ); + return ok(res, { count: rows.length, rows }); + } catch (e) { + return fail(res, 500, "DB_ERROR", "Erreur lecture table pairs", e.message); + } +}); + +/** + * Fetch + insert sur une paire ID + */ +async function fetchAndInsertTickById(pairId, preferredSource = "BINANCE") { + const pairRow = await getPairById(pairId); + const { base, quote } = parsePairCodeFromDb(pairRow.pair_code); + + const data = await getPriceWithFallback(preferredSource, { base, quote }); + + await insertPricePoint({ + pair_id: pairRow.pair_id, + timestamp_ms: data.timestamp_ms, + current_price: data.price, + source: data.source + }); + + return { + pair_id: pairRow.pair_id, + pair_code: pairRow.pair_code, + base, + quote, + source: data.source, + timestamp_ms: data.timestamp_ms, + current_price: data.price, + used_fallback: Boolean(data.used_fallback) + }; +} + +/** + * Prix courant + * GET /api/price/current?pair=BTC/EUR + */ +app.get("/api/price/current", async (req, res) => { + const pair = normalizePairCode(req.query.pair); + + if (!pair) { + return fail(res, 400, "PAIR_REQUIRED", "Parametre 'pair' obligatoire (ex: BTC/EUR)"); + } + + let pair_id; + try { + pair_id = await getActivePairIdByCode(pair); + } catch (err) { + if (err.code === "PAIR_NOT_FOUND") { + return fail(res, 404, "PAIR_NOT_FOUND", `Paire inconnue: ${pair}`); + } + return fail(res, 400, "PAIR_INVALID", `Paire invalide: ${pair}`); + } + + try { + const row = await getCurrentPrice(pair_id); + if (!row) { + return fail(res, 404, "NO_PRICE", `Aucun prix en base pour ${pair}`); + } + + return ok(res, { + pair, + pair_id, + source: row.source, + timestamp_ms: row.timestamp_ms, + current_price: row.current_price, + open_price: row.open_price, + high_price: row.high_price, + low_price: row.low_price, + close_price: row.close_price, + volume_24h: row.volume_24h ?? null + }); + } catch (err) { + return fail(res, 500, "DB_ERROR", "Erreur lecture prix courant", err.message); + } +}); + +/** + * Historique + * GET /api/price/history?pair=BTC/EUR&limit=200 + */ +app.get("/api/price/history", async (req, res) => { + const pair = normalizePairCode(req.query.pair); + const limit = req.query.limit; + + if (!pair) { + return fail(res, 400, "PAIR_REQUIRED", "Parametre 'pair' obligatoire (ex: BTC/EUR)"); + } + + let pair_id; + try { + pair_id = await getActivePairIdByCode(pair); + } catch (err) { + if (err.code === "PAIR_NOT_FOUND") { + return fail(res, 404, "PAIR_NOT_FOUND", `Paire inconnue: ${pair}`); + } + return fail(res, 400, "PAIR_INVALID", `Paire invalide: ${pair}`); + } + + try { + const points = await getPriceHistory(pair_id, limit); + return ok(res, { pair, pair_id, points }); + } catch (err) { + return fail(res, 500, "DB_ERROR", "Erreur lecture historique", err.message); + } +}); + +/** + * Refresh manuel + * POST /api/price/refresh?pair=BTC/EUR + */ +app.post("/api/price/refresh", async (req, res) => { + const pair = normalizePairCode(req.query.pair); + if (!pair) { + return fail(res, 400, "PAIR_REQUIRED", "Parametre 'pair' obligatoire (ex: BTC/EUR)"); + } + + try { + const { base, quote } = parsePairInput(pair); + const pair_id = await getActivePairIdByCode(pair); + + const data = await getPriceWithFallback("BINANCE", { base, quote }); + + await insertPricePoint({ + pair_id, + timestamp_ms: data.timestamp_ms, + current_price: data.price, + source: data.source + }); + + return ok(res, { + pair, + pair_id, + source: data.source, + timestamp_ms: data.timestamp_ms, + current_price: data.price, + used_fallback: Boolean(data.used_fallback) + }); + } catch (err) { + if (err.code === "PAIR_NOT_FOUND") { + return fail(res, 404, "PAIR_NOT_FOUND", `Paire inconnue: ${pair}`); + } + return fail(res, 503, "PROVIDER_ERROR", `Refresh impossible pour ${pair}`, formatProviderError(err)); + } +}); + +/** + * Scheduler: toutes les paires actives à chaque tick + * => limite de concurrence pour éviter rate-limit + */ +async function schedulerTick() { + if (PAIR_IDS_TO_SAMPLE.length === 0) { + console.warn("Aucune paire active à sampler (PAIR_IDS_TO_SAMPLE vide)."); + return; + } + + const preferred = "BINANCE"; + const CONCURRENCY = 5; // ajuste à 3/5/10 selon le nombre de paires + + let i = 0; + + async function worker() { + while (i < PAIR_IDS_TO_SAMPLE.length) { + const pairId = PAIR_IDS_TO_SAMPLE[i++]; + try { + const tick = await fetchAndInsertTickById(pairId, preferred); + console.log( + `Tick OK pair_id=${tick.pair_id} (${tick.pair_code}) ${tick.current_price} (${tick.source}${tick.used_fallback ? " fallback" : ""})` + ); + } catch (err) { + console.error(`Erreur scheduler pair_id=${pairId}: ${formatProviderError(err)}`); + } + } + } + + await Promise.all(Array.from({ length: CONCURRENCY }, worker)); +} + +/** + * ✅ Démarrage du serveur + */ +app.listen(PORT, async () => { + console.log(`Serveur lance : http://localhost:${PORT}`); + + try { + await loadAllActivePairIds(); // charge toutes les paires actives + await schedulerTick(); // 1 tick immédiat + setInterval(schedulerTick, SAMPLE_INTERVAL_MS); + } catch (e) { + console.error("Erreur démarrage serveur:", e.message); + } +}); \ No newline at end of file diff --git a/server/Val_Crypto/services/binance.service.js b/server/Val_Crypto/services/binance.service.js new file mode 100644 index 0000000..f1cce31 --- /dev/null +++ b/server/Val_Crypto/services/binance.service.js @@ -0,0 +1,27 @@ +import axios from "axios"; + +function normalizePair(pair = {}) { + return { + base: String(pair.base || "BTC").toUpperCase(), + quote: String(pair.quote || "EUR").toUpperCase() + }; +} + +export async function getBinancePrice(pair) { + const normalizedPair = normalizePair(pair); + + const res = await axios.get( + "https://api.binance.com/api/v3/ticker/price", + { + timeout: 5000, + params: { + symbol: `${normalizedPair.base}${normalizedPair.quote}` + } + } + ); + + return { + price: parseFloat(res.data.price), + timestamp_ms: Date.now() + }; +} diff --git a/server/Val_Crypto/services/coingecko.service.js b/server/Val_Crypto/services/coingecko.service.js new file mode 100644 index 0000000..0befa88 --- /dev/null +++ b/server/Val_Crypto/services/coingecko.service.js @@ -0,0 +1,39 @@ +const COINGECKO_SYMBOL_TO_ID = { + BTC: "bitcoin", + ETH: "ethereum", + BNB: "binancecoin", + ADA: "cardano", + DOGE: "dogecoin", + LTC: "litecoin" +}; + +function normalizePair(pair = {}) { + return { + base: String(pair.base || "BTC").toUpperCase(), + quote: String(pair.quote || "EUR").toUpperCase() + }; +} + +export async function getCoinGeckoPrice(pair) { + const { base, quote } = normalizePair(pair); + + const vs = quote.toLowerCase(); + const id = COINGECKO_SYMBOL_TO_ID[base]; + if (!id) throw new Error(`CoinGecko: symbole non supporte: ${base}`); + + const url = + `https://api.coingecko.com/api/v3/simple/price?ids=${encodeURIComponent(id)}` + + `&vs_currencies=${encodeURIComponent(vs)}`; + + const res = await fetch(url); + if (!res.ok) throw new Error(`CoinGecko HTTP ${res.status}`); + + const json = await res.json(); + const price = Number(json?.[id]?.[vs]); + if (!Number.isFinite(price)) throw new Error("Prix CoinGecko invalide"); + + return { + price, + timestamp_ms: Date.now() + }; +} \ No newline at end of file diff --git a/server/Val_Crypto/services/price.service.js b/server/Val_Crypto/services/price.service.js new file mode 100644 index 0000000..0e4cd4d --- /dev/null +++ b/server/Val_Crypto/services/price.service.js @@ -0,0 +1,105 @@ +import { getCoinGeckoPrice } from "./coingecko.service.js"; +import { getBinancePrice } from "./binance.service.js"; + +const PRICE_FETCHERS = { + COINGECKO: getCoinGeckoPrice, + BINANCE: getBinancePrice +}; + +export async function getPriceBySource(source, pair) { + const normalizedSource = String(source || "").toUpperCase(); + const fetcher = PRICE_FETCHERS[normalizedSource]; + + if (!fetcher) { + throw new Error(`Source inconnue: ${source}`); + } + + const data = await fetcher(pair); + const price = Number(data.price); + + if (!Number.isFinite(price)) { + throw new Error(`Prix invalide recu depuis ${normalizedSource}`); + } + + return { + source: normalizedSource, + price, + timestamp_ms: Number(data.timestamp_ms) || Date.now() + }; +} + +export function getOtherSource(source) { + const normalizedSource = String(source || "").toUpperCase(); + if (normalizedSource === "COINGECKO") { + return "BINANCE"; + } + if (normalizedSource === "BINANCE") { + return "COINGECKO"; + } + throw new Error(`Source inconnue: ${source}`); +} + +export function formatProviderError(err) { + if (!err) { + return "erreur inconnue"; + } + + const status = err.response?.status; + const statusText = err.response?.statusText; + const code = err.code; + const message = err.message || "erreur sans message"; + + return [status ? `HTTP ${status}` : null, statusText, code, message] + .filter(Boolean) + .join(" | "); +} + +export async function getPriceWithFallback(primarySource, pair) { + const normalizedPrimary = String(primarySource || "").toUpperCase(); + const fallbackSource = getOtherSource(normalizedPrimary); + + try { + return await getPriceBySource(normalizedPrimary, pair); + } catch (primaryError) { + try { + const fallbackData = await getPriceBySource(fallbackSource, pair); + return { + ...fallbackData, + used_fallback: true, + intended_source: normalizedPrimary + }; + } catch (fallbackError) { + const error = new Error( + `Primary(${normalizedPrimary})=${formatProviderError( + primaryError + )} | Fallback(${fallbackSource})=${formatProviderError(fallbackError)}` + ); + error.primaryError = primaryError; + error.fallbackError = fallbackError; + throw error; + } + } +} + +let useCoinGeckoNext = true; + +export async function getAlternatingPrice(pair) { + const orderedSources = useCoinGeckoNext + ? ["COINGECKO", "BINANCE"] + : ["BINANCE", "COINGECKO"]; + + useCoinGeckoNext = !useCoinGeckoNext; + + let lastError = null; + + for (const source of orderedSources) { + try { + return await getPriceBySource(source, pair); + } catch (err) { + lastError = err; + console.log(`${source} indisponible, tentative suivante...`); + } + } + + throw lastError ?? new Error("Aucune API disponible"); +} diff --git a/server/Val_Crypto/test-db.js b/server/Val_Crypto/test-db.js new file mode 100644 index 0000000..2d40658 --- /dev/null +++ b/server/Val_Crypto/test-db.js @@ -0,0 +1,73 @@ +import { getAlternatingPrice } from "./services/price.service.js"; +import { insertCandle } from "./repositories/price.repository.js"; +import { getActivePairId } from "./repositories/pair.repository.js"; +import { db } from "./db.js"; + +const SAMPLE_COUNT = 12; +const SAMPLE_DELAY_MS = 5000; + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function testDbCommunication() { + try { + console.log("TEST COMMUNICATION API -> DB (OHLC reel)"); + + const prices = []; + let lastSource = "UNKNOWN"; + + for (let i = 0; i < SAMPLE_COUNT; i += 1) { + const data = await getAlternatingPrice(); + prices.push(Number(data.price)); + lastSource = data.source; + console.log( + `Sample ${i + 1}/${SAMPLE_COUNT}: ${data.price} via ${data.source}` + ); + if (i < SAMPLE_COUNT - 1) { + await sleep(SAMPLE_DELAY_MS); + } + } + + const open = prices[0]; + const high = Math.max(...prices); + const low = Math.min(...prices); + const close = prices[prices.length - 1]; + + const pairId = await getActivePairId("BTC", "EUR"); + + await insertCandle({ + pair_id: pairId, + source: lastSource, + interval_sec: 300, + timestamp_ms: Date.now(), + open_price: open, + high_price: high, + low_price: low, + close_price: close + }); + + const [rows] = await db.query(` + SELECT + FROM_UNIXTIME(timestamp_ms / 1000) AS candle_time, + source, + interval_sec, + open_price, + high_price, + low_price, + close_price + FROM price_points + WHERE interval_sec = 300 + ORDER BY created_at_ms DESC + LIMIT 1 + `); + + console.log("DONNEE EN BASE :", rows[0]); + process.exit(0); + } catch (err) { + console.error("TEST ECHOUE :", err); + process.exit(1); + } +} + +testDbCommunication();