--- /dev/null
+{
+ "name": "Wallette",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {}
+}
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+/**
+ * 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
"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",
"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"
}
}
--- /dev/null
+{
+ "name": "pdw25-26",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {}
+}