โ† ะะฐะทะฐะด
'use strict'; const express = require('express'); const prisma = require('../services/db'); const { checkApiKey } = require('../middleware/auth'); const logger = require('../utils/logger'); const router = express.Router(); // โ”€โ”€โ”€ GET /api/backtest/stats โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // Aggregate stats: WR%, avg P&L per strategy, per underlying router.get('/api/backtest/stats', checkApiKey, async (req, res) => { try { const days = Math.min(parseInt(req.query.days) || 30, 90); const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000); const allLogs = await prisma.signalLog.findMany({ where: { createdAt: { gte: since } }, orderBy: { createdAt: 'desc' }, }); const completed = allLogs.filter(s => s.outcome !== null); const pending = allLogs.filter(s => s.outcome === null); // โ”€โ”€โ”€ Per-Strategy stats โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const strategyMap = {}; for (const sig of completed) { if (!strategyMap[sig.strategy]) { strategyMap[sig.strategy] = { total: 0, wins: 0, losses: 0, pnl1h: [], pnl4h: [], pnl24h: [] }; } const s = strategyMap[sig.strategy]; s.total++; if (sig.outcome === 'WIN') s.wins++; else if (sig.outcome === 'LOSS') s.losses++; if (sig.pnlPct1h != null) s.pnl1h.push(sig.pnlPct1h); if (sig.pnlPct4h != null) s.pnl4h.push(sig.pnlPct4h); if (sig.pnlPct24h != null) s.pnl24h.push(sig.pnlPct24h); } const strategies = Object.entries(strategyMap).map(([name, s]) => ({ strategy: name, total: s.total, wins: s.wins, losses: s.losses, winRate: s.total > 0 ? Math.round((s.wins / s.total) * 100) : 0, avgPnl1h: avg(s.pnl1h), avgPnl4h: avg(s.pnl4h), avgPnl24h: avg(s.pnl24h), })).sort((a, b) => b.winRate - a.winRate); // โ”€โ”€โ”€ Per-Underlying stats โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const underlyingMap = {}; for (const sig of completed) { if (!underlyingMap[sig.underlying]) { underlyingMap[sig.underlying] = { total: 0, wins: 0, pnl24h: [] }; } const u = underlyingMap[sig.underlying]; u.total++; if (sig.outcome === 'WIN') u.wins++; if (sig.pnlPct24h != null) u.pnl24h.push(sig.pnlPct24h); } const underlyings = Object.entries(underlyingMap).map(([name, u]) => ({ underlying: name, total: u.total, wins: u.wins, winRate: u.total > 0 ? Math.round((u.wins / u.total) * 100) : 0, avgPnl24h: avg(u.pnl24h), })).sort((a, b) => b.total - a.total); // โ”€โ”€โ”€ Overall โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const totalCompleted = completed.length; const totalWins = completed.filter(s => s.outcome === 'WIN').length; res.json({ success: true, data: { period: `${days}d`, totalSignals: allLogs.length, completed: totalCompleted, pending: pending.length, overallWinRate: totalCompleted > 0 ? Math.round((totalWins / totalCompleted) * 100) : 0, avgPnl24h: avg(completed.filter(s => s.pnlPct24h != null).map(s => s.pnlPct24h)), strategies, underlyings, }, }); } catch (err) { logger.error(`GET /api/backtest/stats error: ${err.message}`); res.status(500).json({ success: false, error: err.message }); } }); // โ”€โ”€โ”€ GET /api/backtest/signals โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // Raw signal log with outcomes (paginated) router.get('/api/backtest/signals', checkApiKey, async (req, res) => { try { const limit = Math.min(parseInt(req.query.limit) || 50, 200); const offset = parseInt(req.query.offset) || 0; const strategy = req.query.strategy; const underlying = req.query.underlying; const outcome = req.query.outcome; // WIN, LOSS, PENDING const where = {}; if (strategy) where.strategy = strategy; if (underlying) where.underlying = underlying; if (outcome === 'PENDING') where.outcome = null; else if (outcome) where.outcome = outcome; const [signals, total] = await Promise.all([ prisma.signalLog.findMany({ where, orderBy: { createdAt: 'desc' }, take: limit, skip: offset, }), prisma.signalLog.count({ where }), ]); res.json({ success: true, data: { signals: signals.map(s => ({ ...s, parameters: safeParse(s.parameters), })), total, limit, offset, }, }); } catch (err) { logger.error(`GET /api/backtest/signals error: ${err.message}`); res.status(500).json({ success: false, error: err.message }); } }); function avg(arr) { if (!arr || arr.length === 0) return 0; return parseFloat((arr.reduce((a, b) => a + b, 0) / arr.length).toFixed(2)); } function safeParse(str) { try { return JSON.parse(str); } catch { return {}; } } module.exports = router;