← Back
'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;

📜 Git History

e12be00feat: Historical Signals Backtest tracking (Task 2.4)3 months ago
Show last diff
Loading...