]> git.digitality.be Git - pdw25-26/commitdiff
Module Strategy v0.1.0
authorSacheat <sacha.libion@outlook.com>
Thu, 26 Feb 2026 19:51:11 +0000 (20:51 +0100)
committerSacheat <sacha.libion@outlook.com>
Thu, 26 Feb 2026 19:51:11 +0000 (20:51 +0100)
14 files changed:
Wallette/package-lock.json [new file with mode: 0644]
Wallette/server/modules/strategy/config/enum.js [new file with mode: 0644]
Wallette/server/modules/strategy/factories/StrategyFactory.js [new file with mode: 0644]
Wallette/server/modules/strategy/index.js [new file with mode: 0644]
Wallette/server/modules/strategy/repositories/MarketDataRepo.js [new file with mode: 0644]
Wallette/server/modules/strategy/repositories/SignalRepo.js [new file with mode: 0644]
Wallette/server/modules/strategy/repositories/StrategyRepo.js [new file with mode: 0644]
Wallette/server/modules/strategy/services/BotService.js [new file with mode: 0644]
Wallette/server/modules/strategy/strategies/GreedyStrategy.js [new file with mode: 0644]
Wallette/server/modules/strategy/strategies/StandardStrategy.js [new file with mode: 0644]
Wallette/server/modules/strategy/strategies/TradingStrategy.js [new file with mode: 0644]
Wallette/server/package-lock.json
Wallette/server/package.json
package-lock.json [new file with mode: 0644]

diff --git a/Wallette/package-lock.json b/Wallette/package-lock.json
new file mode 100644 (file)
index 0000000..2ceb582
--- /dev/null
@@ -0,0 +1,6 @@
+{
+  "name": "Wallette",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {}
+}
diff --git a/Wallette/server/modules/strategy/config/enum.js b/Wallette/server/modules/strategy/config/enum.js
new file mode 100644 (file)
index 0000000..d6a331c
--- /dev/null
@@ -0,0 +1,14 @@
+const StrategyProfile = Object.freeze({
+    SAFE: 'SAFE',
+    STANDARD: 'STANDARD',
+    GREEDY: 'GREEDY'
+});
+
+const ActionTypes = Object.freeze({
+    BUY_ALERT: 'BUY',
+    SELL_ALERT: 'SELL',
+    HOLD_ALERT: 'HOLD',
+    STOP_LOSS: 'STOP_LOSS'
+});
+
+export { StrategyProfile, ActionTypes };
\ No newline at end of file
diff --git a/Wallette/server/modules/strategy/factories/StrategyFactory.js b/Wallette/server/modules/strategy/factories/StrategyFactory.js
new file mode 100644 (file)
index 0000000..21c8b67
--- /dev/null
@@ -0,0 +1,23 @@
+import { StrategyProfile } from '../config/enum.js';
+import StandardStrategy from '../strategies/StandardStrategy.js';
+import GreedyStrategy from '../strategies/GreedyStrategy.js';
+
+class StrategyFactory {
+
+    static getStrategy(mode) {
+        switch (mode) {
+            case StrategyProfile.SAFE:
+            case StrategyProfile.STANDARD:
+                return new StandardStrategy();
+
+            case StrategyProfile.GREEDY:
+                return new GreedyStrategy();
+
+            default:
+                console.log(`Mode inconnu : ${mode}, utilisation de STANDARD.`);
+                return new StandardStrategy();
+        }
+    }
+}
+
+export default StrategyFactory;
\ No newline at end of file
diff --git a/Wallette/server/modules/strategy/index.js b/Wallette/server/modules/strategy/index.js
new file mode 100644 (file)
index 0000000..2aaf72d
--- /dev/null
@@ -0,0 +1,25 @@
+import dotenv from 'dotenv';
+dotenv.config();
+import cron from'node-cron';
+import botService from './services/BotService.js';
+import pool from '../../config/db.js';
+
+console.log("CryptoBot Backend Démarré !");
+console.log("Planificateur armé : Toutes les 5 minutes (*/5 * * * *)");
+
+// Configuration du Cron Job
+// "*/5" = Toutes les 5 minutes.
+// Pour la démo, toutes les minutes pour voir le résultat vite.
+cron.schedule('*/1 * * * *', async () => {
+
+    // On lance le service
+    await botService.runAnalysisBatch();
+
+});
+
+// Gestion propre de l'arrêt (CTRL+C)
+process.on('SIGINT', async () => {
+    console.log("\nArrêt du bot...");
+    await pool.end();
+    process.exit(0);
+});
\ No newline at end of file
diff --git a/Wallette/server/modules/strategy/repositories/MarketDataRepo.js b/Wallette/server/modules/strategy/repositories/MarketDataRepo.js
new file mode 100644 (file)
index 0000000..1012ec8
--- /dev/null
@@ -0,0 +1,42 @@
+import pool from '../../../config/db.js';
+import StrategyRepo from './StrategyRepo.js';
+
+class MarketDataRepo {
+
+    /**
+     * Récupère les dernières bougies pour une paire donnée.
+     * @param {number} pairId - L'ID de la paire (ex: 1 pour BTC)
+     * @param {number} limit - Nombre de bougies à récupérer (ex: 100)
+     * @returns {Promise<Array>} Liste triée du plus ANCIEN au plus RÉCENT
+     */
+    async getLastCandles(pairId, limit = 100) {
+        const sql = `
+            SELECT 
+                timestamp_ms,
+                open_price,
+                high_price,
+                low_price,
+                close_price,
+                volume
+            FROM price_points
+            WHERE pair_id = ?
+            ORDER BY timestamp_ms DESC
+            LIMIT ?
+        `;
+
+        try {
+            const [rows] = await pool.query(sql, [pairId, limit]);
+
+            // Pour calculer un RSI ou une SMA, on a besoin de l'ordre chronologique.
+            // On inverse donc le tableau : [Vieux ..... Récent]
+            return rows.reverse();
+
+        } catch (error) {
+            console.error(`Erreur MarketDataRepo (Pair ${pairId}) :`, error.message);
+            throw error;
+        }
+    }
+}
+
+const marketDataRepo = new MarketDataRepo();
+export default marketDataRepo;
\ No newline at end of file
diff --git a/Wallette/server/modules/strategy/repositories/SignalRepo.js b/Wallette/server/modules/strategy/repositories/SignalRepo.js
new file mode 100644 (file)
index 0000000..b9adfd5
--- /dev/null
@@ -0,0 +1,49 @@
+import pool from '../../../config/db.js';
+import { v4 as uuidv4 } from 'uuid';
+
+class SignalRepo {
+
+    async saveSignal(signalData) {
+        const sql = `
+            INSERT INTO signals (
+                signal_id,
+                user_strategy_id,
+                timestamp_ms,
+                action,
+                confidence,
+                reason,
+                price_at_signal,
+                indicators,
+                created_at_ms
+            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+        `;
+
+        const id = uuidv4();
+        const now = Date.now();
+        const indicatorsJson = JSON.stringify(signalData.indicators);
+
+        const params = [
+            id,
+            signalData.userStrategyId,
+            now,
+            signalData.action,
+            signalData.confidence,
+            signalData.reason,
+            signalData.price,
+            indicatorsJson,
+            now
+        ];
+
+        try {
+            await pool.query(sql, params);
+            console.log(`Signal de ${signalData.action} sauvegardé en DB avec succès (ID: ${id})`);
+            return id;
+        } catch (error) {
+            console.error("Erreur sauvegarde Signal :", error.message);
+            throw error;
+        }
+    }
+}
+
+const signalRepo = new SignalRepo();
+export default signalRepo;
\ No newline at end of file
diff --git a/Wallette/server/modules/strategy/repositories/StrategyRepo.js b/Wallette/server/modules/strategy/repositories/StrategyRepo.js
new file mode 100644 (file)
index 0000000..5a353ae
--- /dev/null
@@ -0,0 +1,31 @@
+import pool from '../../../config/db.js';
+
+class StrategyRepo {
+
+    /**
+     * Récupère toutes les stratégies actives des utilisateurs.
+     * @returns {Promise<Array>} Liste des configurations
+     */
+    async getActiveStrategies() {
+        const sql = `
+            SELECT user_strategy_id,
+                   user_id,
+                   pair_id,
+                   mode,
+                   params
+            FROM user_strategies
+            WHERE is_active = TRUE
+        `;
+
+        try {
+            const [rows] = await pool.query(sql);
+            return rows;
+        } catch (error) {
+            console.error("Erreur SQL (getActiveStrategies) :", error.message);
+            throw error;
+        }
+    }
+}
+
+const strategyRepo = new StrategyRepo();
+export default strategyRepo;
\ No newline at end of file
diff --git a/Wallette/server/modules/strategy/services/BotService.js b/Wallette/server/modules/strategy/services/BotService.js
new file mode 100644 (file)
index 0000000..599d089
--- /dev/null
@@ -0,0 +1,74 @@
+import strategyRepo from '../repositories/StrategyRepo.js';
+import marketRepo from '../repositories/MarketDataRepo.js';
+import signalRepo from '../repositories/SignalRepo.js';
+import StrategyFactory from '../factories/StrategyFactory.js';
+
+class BotService {
+
+    /**
+     * Lance un cycle complet d'analyse pour tous les utilisateurs.
+     */
+    async runAnalysisBatch() {
+        console.log(`\nDémarrage du cycle d'analyse : ${new Date().toLocaleTimeString()}`);
+
+        try {
+            // 1. Récupérer toutes les stratégies actives
+            const strategies = await strategyRepo.getActiveStrategies();
+            console.log(`${strategies.length} stratégies utilisateurs à traiter.`);
+
+            // 2. Pour chaque utilisateur...
+            for (const userConfig of strategies) {
+                await this.processUserStrategy(userConfig);
+            }
+
+            console.log("Cycle terminé.\n");
+
+        } catch (error) {
+            console.error("Erreur critique du cycle :", error);
+        }
+    }
+
+    /**
+     * Traite un utilisateur spécifique
+     */
+    async processUserStrategy(userConfig) {
+        const { user_strategy_id, pair_id, mode, params } = userConfig;
+
+        try {
+            // A. Récupérer les données de marché
+            const candles = await marketRepo.getLastCandles(pair_id, 100);
+
+            if (candles.length === 0) {
+                console.log(`Pas de données pour la paire ${pair_id}`);
+                return;
+            }
+
+            // B. Instancier la bonne stratégie via la Factory
+            const strategyAlgo = StrategyFactory.getStrategy(mode);
+
+            // C. Lancer l'analyse
+            // Note : params est un JSON qui vient de la DB (ex: {rsi_period: 14})
+            const signal = strategyAlgo.analyze(candles, params);
+
+            // D. Si un signal est trouvé, sauvegarde
+            if (signal) {
+                console.log(`SIGNAL DÉTECTÉ pour ${user_strategy_id} (${mode}) !`);
+
+                await signalRepo.saveSignal({
+                    userStrategyId: user_strategy_id,
+                    action: signal.action,
+                    confidence: signal.confidence,
+                    reason: signal.reason,
+                    price: candles[candles.length - 1].close_price,
+                    indicators: signal.indicators
+                });
+            }
+
+        } catch (error) {
+            console.error(`Erreur sur la stratégie ${user_strategy_id} :`, error.message);
+        }
+    }
+}
+
+const botService = new BotService();
+export default botService;
\ No newline at end of file
diff --git a/Wallette/server/modules/strategy/strategies/GreedyStrategy.js b/Wallette/server/modules/strategy/strategies/GreedyStrategy.js
new file mode 100644 (file)
index 0000000..7aeabc6
--- /dev/null
@@ -0,0 +1,47 @@
+import TradingStrategy from './TradingStrategy.js';
+import { ActionTypes } from '../config/enum.js';
+import { EMA } from 'technicalindicators';
+
+class GreedyStrategy extends TradingStrategy {
+    constructor() {
+        super('GREEDY');
+    }
+
+    analyze(candles, userParams) {
+        const prices = candles.map(c => parseFloat(c.close_price));
+
+        // On récupère les paramètres de la DB
+        const shortPeriod = userParams?.ema_short || 9;
+        const longPeriod = userParams?.ema_long || 21;
+
+        // Sécurité : A-t-on assez de bougies ?
+        if (prices.length < longPeriod) {
+            console.log(`Pas assez de données pour ${this.name} (Requis: ${longPeriod}, Reçu: ${prices.length})`);
+            return null;
+        }
+
+        // Calcul des deux courbes EMA
+        const emaShortValues = EMA.calculate({ period: shortPeriod, values: prices });
+        const emaLongValues = EMA.calculate({ period: longPeriod, values: prices });
+
+        const lastShort = emaShortValues[emaShortValues.length - 1];
+        const lastLong = emaLongValues[emaLongValues.length - 1];
+        const currentPrice = prices[prices.length - 1];
+
+        console.log(`Analyse ${this.name} -> Prix: ${currentPrice} | EMA Courte(${shortPeriod}): ${lastShort.toFixed(2)} | EMA Longe(${longPeriod}): ${lastLong.toFixed(2)}`);
+
+        // Logique GREEDY : L'EMA courte passe au-dessus de l'EMA longue (Croisement haussier)
+        if (lastShort > lastLong) {
+            return {
+                action: ActionTypes.BUY_ALERT,
+                reason: `Croisement Haussier Agressif (EMA ${shortPeriod} > EMA ${longPeriod})`,
+                confidence: 0.9,
+                indicators: { ema_short: lastShort, ema_long: lastLong }
+            };
+        }
+
+        return null;
+    }
+}
+
+export default GreedyStrategy;
\ No newline at end of file
diff --git a/Wallette/server/modules/strategy/strategies/StandardStrategy.js b/Wallette/server/modules/strategy/strategies/StandardStrategy.js
new file mode 100644 (file)
index 0000000..17a0c6f
--- /dev/null
@@ -0,0 +1,54 @@
+import TradingStrategy from './TradingStrategy.js';
+import { ActionTypes } from '../config/enum.js'
+import { RSI, SMA } from 'technicalindicators';
+
+class StandardStrategy extends TradingStrategy {
+    constructor() {
+        super('STANDARD');
+    }
+
+    analyze(candles, userParams) {
+        // Extrait le prix de fermeture
+        const prices = candles.map(c => parseFloat(c.close_price));
+
+        // Récupération des params (ou valeurs par défaut pour le test)
+        // En prod, ce sera RSI=14 et SMA=50
+        const rsiPeriod = userParams?.rsi_period || 14;
+        const smaPeriod = userParams?.sma_period || 50;
+
+        // Vérification de sécurité : A-t-on assez de données ?
+        if (prices.length < smaPeriod) {
+            console.log(`Pas assez de données pour ${this.name} (Requis: ${smaPeriod}, Reçu: ${prices.length})`);
+            return null;
+        }
+
+        // Calcul mathémathique
+        const rsiValues = RSI.calculate({ period: rsiPeriod, values: prices });
+        const smaValues = SMA.calculate({ period: smaPeriod, values: prices });
+
+        // On récup les dernières valeurs calculées
+        const lastRSI = rsiValues[rsiValues.length - 1];
+        const lastSMA = smaValues[smaValues.length - 1];
+        const currentPrice = prices[prices.length - 1];
+
+        console.log(`Analyse ${this.name} -> Prix: ${currentPrice} | SMA(${smaPeriod}): ${lastSMA} | RSI(${rsiPeriod}): ${lastRSI}`);
+
+        // 3. Logique de décision
+        // Achat si : Prix au-dessus de la SMA (Tendance Haussière) ET RSI bas (Pas cher)
+        // Seuil RSI standard = 35 (ici j'ai mis 100 pour forcer le signal lors du test !)
+        const rsiThreshold = 35;
+
+        if (currentPrice > lastSMA && lastRSI < rsiThreshold) {
+            return {
+                action: ActionTypes.BUY_ALERT,
+                reason: `Tendance Haussière (Prix > SMA) & RSI Favorable (${lastRSI.toFixed(2)})`,
+                confidence: 0.8,
+                indicators: { rsi: lastRSI, sma: lastSMA }
+            };
+        }
+
+        return null;
+    }
+}
+
+export default StandardStrategy;
\ No newline at end of file
diff --git a/Wallette/server/modules/strategy/strategies/TradingStrategy.js b/Wallette/server/modules/strategy/strategies/TradingStrategy.js
new file mode 100644 (file)
index 0000000..c8c4772
--- /dev/null
@@ -0,0 +1,21 @@
+/**
+ * Classe "INTERFACE"
+ * Définit le squelette d'une stratégie.
+ */
+class TradingStrategy {
+    constructor(name) {
+        this.name = name;
+    }
+
+    /**
+     * Méthode principale à implémenter par les enfants.
+     * @param {Array} candles - Liste des bougies (Close price, etc.)
+     * @param {Object} userParams - Paramètres spécifiques (ex: { rsi_period: 14 })
+     * @returns {Object|null} - Renvoie un objet Signal ou null si rien à faire
+     */
+    analyze(candles, userParams) {
+        throw new Error(`La stratégie '${this.name}' n'a pas implémenté la méthode analyze() !`);
+    }
+}
+
+export default TradingStrategy;
\ No newline at end of file
index 4ca2e4c5d4e0475042f07c37cf905217e0cecd89..dbca677ce2becbf34a9481f1b0b4e0178d704d66 100644 (file)
       "license": "ISC",
       "dependencies": {
         "cors": "^2.8.5",
-        "dotenv": "^16.4.5",
+        "dotenv": "^16.6.1",
         "express": "^4.18.2",
-        "mysql2": "^3.6.5",
+        "mysql2": "^3.18.2",
+        "node-cron": "^4.2.1",
         "nodemailer": "^8.0.1",
         "socket.io": "^4.8.3",
+        "technicalindicators": "^3.1.0",
         "uuid": "^9.0.1"
       }
     },
       "license": "MIT"
     },
     "node_modules/mysql2": {
-      "version": "3.17.0",
-      "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.17.0.tgz",
-      "integrity": "sha512-qF1KYPuytBGqAnMzaQ5/rW90iIqcjnrnnS7bvcJcdarJzlUTAiD9ZC0T7mwndacECseSQ6LcRbRvryXLp25m+g==",
+      "version": "3.18.2",
+      "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.18.2.tgz",
+      "integrity": "sha512-UfEShBFAZZEAKjySnTUuE7BgqkYT4mx+RjoJ5aqtmwSSvNcJ/QxQPXz/y3jSxNiVRedPfgccmuBtiPCSiEEytw==",
       "license": "MIT",
       "dependencies": {
         "aws-ssl-profiles": "^1.1.2",
         "generate-function": "^2.3.1",
         "iconv-lite": "^0.7.2",
         "long": "^5.3.2",
-        "lru.min": "^1.1.3",
+        "lru.min": "^1.1.4",
         "named-placeholders": "^1.1.6",
-        "seq-queue": "^0.0.5",
-        "sql-escaper": "^1.3.1"
+        "sql-escaper": "^1.3.3"
       },
       "engines": {
         "node": ">= 8.0"
+      },
+      "peerDependencies": {
+        "@types/node": ">= 8"
       }
     },
     "node_modules/mysql2/node_modules/iconv-lite": {
         "node": ">= 0.6"
       }
     },
+    "node_modules/node-cron": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz",
+      "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
     "node_modules/nodemailer": {
       "version": "8.0.1",
       "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz",
       "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
       "license": "MIT"
     },
-    "node_modules/seq-queue": {
-      "version": "0.0.5",
-      "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
-      "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
-    },
     "node_modules/serve-static": {
       "version": "1.16.3",
       "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
       "license": "MIT"
     },
     "node_modules/sql-escaper": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.1.tgz",
-      "integrity": "sha512-GLMJGWKzrr7BS5E5+8Prix6RGfBd4UokKMxkPSg313X0TvUyjdJU3Xg7FAhhcba4dHnLy81t4YeHETKLGVsDow==",
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz",
+      "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==",
       "license": "MIT",
       "engines": {
         "bun": ">=1.0.0",
         "node": ">= 0.8"
       }
     },
+    "node_modules/technicalindicators": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/technicalindicators/-/technicalindicators-3.1.0.tgz",
+      "integrity": "sha512-f16mOc+Y05hNy/of+UbGxhxQQmxUztCiluhsqC5QLUYz4WowUgKde9m6nIjK1Kay0wGHigT0IkOabpp0+22UfA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "^6.0.96"
+      }
+    },
+    "node_modules/technicalindicators/node_modules/@types/node": {
+      "version": "6.14.13",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-6.14.13.tgz",
+      "integrity": "sha512-J1F0XJ/9zxlZel5ZlbeSuHW2OpabrUAqpFuC2sm2I3by8sERQ8+KCjNKUcq8QHuzpGMWiJpo9ZxeHrqrP2KzQw==",
+      "license": "MIT"
+    },
     "node_modules/toidentifier": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
index 53e6e860f5fd7cf7fdde8223e88b5ea9783e7115..877175093938dd59ac1cc35c96bb3809307659c0 100644 (file)
   "license": "ISC",
   "dependencies": {
     "cors": "^2.8.5",
-    "dotenv": "^16.4.5",
+    "dotenv": "^16.6.1",
     "express": "^4.18.2",
-    "mysql2": "^3.6.5",
+    "mysql2": "^3.18.2",
+    "node-cron": "^4.2.1",
     "nodemailer": "^8.0.1",
     "socket.io": "^4.8.3",
+    "technicalindicators": "^3.1.0",
     "uuid": "^9.0.1"
   }
 }
diff --git a/package-lock.json b/package-lock.json
new file mode 100644 (file)
index 0000000..c9e11c0
--- /dev/null
@@ -0,0 +1,6 @@
+{
+  "name": "pdw25-26",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {}
+}