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
+ * Schéma officiel du projet (table: price_points)
+ * Colonnes utilisées:
+ * - current_price
+ * - volume_24h
+ * - candle_close
+ * (+ métadonnées habituelles: price_id, pair_id, source, timestamp_ms, created_at_ms)
+ */
+
+/**
+ * Insère un point de prix.
*
- * ⚠️ Pour que high/low varient, il faut appeler insertPricePoint
- * plus souvent que toutes les 5 minutes (ex: toutes les 10–30s).
+ * NOTE: On n'utilise pas de colonnes OHLC (open/high/low/close) car elles n'existent
+ * pas dans le schéma du projet.
*/
export async function insertPricePoint({
pair_id,
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)");
+ throw new Error("volume_24h 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;
+ const closeValue = candle_close == null ? price : Number(candle_close);
+ if (!Number.isFinite(closeValue) || closeValue <= 0) {
+ throw new Error("candle_close invalide (>0)");
}
- // 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;
+ const priceId = crypto.randomUUID();
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 = ?
+ INSERT INTO price_points (
+ price_id,
+ pair_id,
+ source,
+ timestamp_ms,
+ current_price,
+ volume_24h,
+ candle_close,
+ created_at_ms
+ )
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`,
- [bucketStart, newHigh, newLow, closeValue, vol, Date.now(), row.price_id]
+ [priceId, pid, src, ts, price, vol, closeValue, Date.now()]
);
}
/**
- * Prix courant = dernière bougie (close_price)
+ * Prix courant = dernier point (ORDER BY timestamp_ms DESC, created_at_ms DESC)
*/
export async function getCurrentPrice(pair_id) {
const pid = Number(pair_id);
SELECT
source,
timestamp_ms,
- open_price,
- high_price,
- low_price,
- close_price,
- volume
+ current_price,
+ volume_24h,
+ candle_close
FROM price_points
WHERE pair_id = ?
ORDER BY timestamp_ms DESC, created_at_ms DESC
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
+ current_price: r.current_price,
+ volume_24h: r.volume_24h ?? null,
+ candle_close: r.candle_close ?? null
};
}
/**
- * Historique = dernières bougies (OHLC)
+ * Historique = derniers points de prix.
*/
export async function getPriceHistory(pair_id, limit = 200) {
const pid = Number(pair_id);
SELECT
source,
timestamp_ms,
- open_price,
- high_price,
- low_price,
- close_price,
- volume
+ current_price,
+ volume_24h,
+ candle_close
FROM price_points
WHERE pair_id = ?
ORDER BY timestamp_ms DESC, created_at_ms DESC
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
+ current_price: r.current_price,
+ volume_24h: r.volume_24h ?? null,
+ candle_close: r.candle_close ?? null
}));
-}
\ No newline at end of file
+}
+++ /dev/null
-// =========================================================
-// PRICE SERVICE - Serveur standalone
-// =========================================================
-// Ce fichier encapsule le module price
-// en microservice Express indépendant sur port 3001.
-//
-// USAGE : node modules/price/server.js (depuis Wallette/server/)
-// PORT : process.env.PRICE_PORT || 3001
-// =========================================================
-
-import cors from 'cors';
-import dotenv from 'dotenv';
-import express from 'express';
-import path from 'path';
-import { fileURLToPath } from 'url';
-
-// Charger .env depuis Wallette/server/.env
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = path.dirname(__filename);
-dotenv.config({ path: path.resolve(__dirname, '../../.env') });
-
-// IMPORTANT : dotenv doit être chargé AVANT l'import du router
-// car db.js lit process.env au moment de l'import
-const { default: priceApiRouter } = await import('./routes/api.router.js');
-
-const PORT = process.env.PRICE_PORT || 3001;
-
-const app = express();
-app.use(cors());
-app.use(express.json());
-app.use(express.static(path.join(__dirname, 'public')));
-
-// ─── Health check (requis par le gateway) ────────────────
-app.get('/health', (req, res) => {
- res.json({ ok: true, service: 'price-service', port: PORT });
-});
-
-// ─── Routes API price ─────────────────────────────────────
-// Le router expose /price/current, /price/history...
-// On le monte sur /api pour que le gateway route /api/price/* correctement
-app.use('/api', priceApiRouter);
-
-// ─── 404 ─────────────────────────────────────────────────
-app.use((req, res) => {
- res.status(404).json({ ok: false, error: { code: 'NOT_FOUND', message: 'Route non trouvée' } });
-});
-
-// ─── Démarrage ────────────────────────────────────────────
-app.listen(PORT, () => {
- console.log(`
- ╔══════════════════════════════════════════╗
- ║ PRICE SERVICE ║
- ║ HTTP : http://localhost:${PORT} ║
- ║ → /health = OK ║
- ║ → /api/price/current?pair= = prix ║
- ║ → /api/price/history?pair= = historique ║
- ╚══════════════════════════════════════════╝`);
-});