From fb0ecfb330d6fcf3227aa4e9bb90d2b05d898d39 Mon Sep 17 00:00:00 2001 From: Sacheat Date: Thu, 26 Feb 2026 20:51:11 +0100 Subject: [PATCH] Module Strategy v0.1.0 --- Wallette/package-lock.json | 6 ++ .../server/modules/strategy/config/enum.js | 14 ++++ .../strategy/factories/StrategyFactory.js | 23 ++++++ Wallette/server/modules/strategy/index.js | 25 +++++++ .../strategy/repositories/MarketDataRepo.js | 42 +++++++++++ .../strategy/repositories/SignalRepo.js | 49 ++++++++++++ .../strategy/repositories/StrategyRepo.js | 31 ++++++++ .../modules/strategy/services/BotService.js | 74 +++++++++++++++++++ .../strategy/strategies/GreedyStrategy.js | 47 ++++++++++++ .../strategy/strategies/StandardStrategy.js | 54 ++++++++++++++ .../strategy/strategies/TradingStrategy.js | 21 ++++++ Wallette/server/package-lock.json | 55 ++++++++++---- Wallette/server/package.json | 6 +- package-lock.json | 6 ++ 14 files changed, 435 insertions(+), 18 deletions(-) create mode 100644 Wallette/package-lock.json create mode 100644 Wallette/server/modules/strategy/config/enum.js create mode 100644 Wallette/server/modules/strategy/factories/StrategyFactory.js create mode 100644 Wallette/server/modules/strategy/index.js create mode 100644 Wallette/server/modules/strategy/repositories/MarketDataRepo.js create mode 100644 Wallette/server/modules/strategy/repositories/SignalRepo.js create mode 100644 Wallette/server/modules/strategy/repositories/StrategyRepo.js create mode 100644 Wallette/server/modules/strategy/services/BotService.js create mode 100644 Wallette/server/modules/strategy/strategies/GreedyStrategy.js create mode 100644 Wallette/server/modules/strategy/strategies/StandardStrategy.js create mode 100644 Wallette/server/modules/strategy/strategies/TradingStrategy.js create mode 100644 package-lock.json diff --git a/Wallette/package-lock.json b/Wallette/package-lock.json new file mode 100644 index 0000000..2ceb582 --- /dev/null +++ b/Wallette/package-lock.json @@ -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 index 0000000..d6a331c --- /dev/null +++ b/Wallette/server/modules/strategy/config/enum.js @@ -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 index 0000000..21c8b67 --- /dev/null +++ b/Wallette/server/modules/strategy/factories/StrategyFactory.js @@ -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 index 0000000..2aaf72d --- /dev/null +++ b/Wallette/server/modules/strategy/index.js @@ -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 index 0000000..1012ec8 --- /dev/null +++ b/Wallette/server/modules/strategy/repositories/MarketDataRepo.js @@ -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} 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 index 0000000..b9adfd5 --- /dev/null +++ b/Wallette/server/modules/strategy/repositories/SignalRepo.js @@ -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 index 0000000..5a353ae --- /dev/null +++ b/Wallette/server/modules/strategy/repositories/StrategyRepo.js @@ -0,0 +1,31 @@ +import pool from '../../../config/db.js'; + +class StrategyRepo { + + /** + * Récupère toutes les stratégies actives des utilisateurs. + * @returns {Promise} 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 index 0000000..599d089 --- /dev/null +++ b/Wallette/server/modules/strategy/services/BotService.js @@ -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 index 0000000..7aeabc6 --- /dev/null +++ b/Wallette/server/modules/strategy/strategies/GreedyStrategy.js @@ -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 index 0000000..17a0c6f --- /dev/null +++ b/Wallette/server/modules/strategy/strategies/StandardStrategy.js @@ -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 index 0000000..c8c4772 --- /dev/null +++ b/Wallette/server/modules/strategy/strategies/TradingStrategy.js @@ -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 diff --git a/Wallette/server/package-lock.json b/Wallette/server/package-lock.json index 4ca2e4c..dbca677 100644 --- a/Wallette/server/package-lock.json +++ b/Wallette/server/package-lock.json @@ -10,11 +10,13 @@ "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" } }, @@ -692,9 +694,9 @@ "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", @@ -702,13 +704,15 @@ "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": { @@ -748,6 +752,15 @@ "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", @@ -913,11 +926,6 @@ "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", @@ -1122,9 +1130,9 @@ "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", @@ -1145,6 +1153,21 @@ "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", diff --git a/Wallette/server/package.json b/Wallette/server/package.json index 53e6e86..8771750 100644 --- a/Wallette/server/package.json +++ b/Wallette/server/package.json @@ -22,11 +22,13 @@ "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 index 0000000..c9e11c0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "pdw25-26", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} -- 2.50.1