← back to Bertha

src/app/api/models/route.js

109 lines

import { NextResponse } from 'next/server';
const { query } = require('../../../lib/db');
const { generatePrediction, storePrediction } = require('../../../lib/model');
const { calculateEdge } = require('../../../lib/risk');

export const dynamic = 'force-dynamic';

export async function GET() {
  try {
    const [models, predictions, calibration] = await Promise.all([
      query('SELECT * FROM model_versions ORDER BY id DESC'),
      query('SELECT * FROM predictions ORDER BY asof_ts DESC LIMIT 50'),
      query('SELECT * FROM calibration_log ORDER BY eval_ts DESC LIMIT 20'),
    ]);
    return NextResponse.json({
      models: models.rows,
      predictions: predictions.rows,
      calibration: calibration.rows,
    });
  } catch (err) {
    return NextResponse.json({ error: err.message }, { status: 500 });
  }
}

export async function POST(req) {
  const body = await req.json();

  if (body.action === 'run_predictions') {
    try {
      // Get all active markets with parsed events
      const marketsRes = await query(`
        SELECT m.*, t.asset_id, t.price as token_price
        FROM markets m
        LEFT JOIN tokens t ON t.condition_id = m.condition_id AND t.outcome = 'Yes'
        WHERE m.active = TRUE AND m.parsed_event IS NOT NULL
      `);

      let predictionsMade = 0;
      const now = new Date();
      const month = now.getMonth() + 1;

      for (const market of marketsRes.rows) {
        const event = market.parsed_event;
        if (!event?.variable || !event?.threshold) continue;

        // Get latest forecast data for this market's location
        let forecastData = null;
        if (event.location) {
          const forecastRes = await query(`
            SELECT * FROM forecast_runs
            WHERE location_name = $1 AND variable = $2
            ORDER BY run_ts DESC LIMIT 20
          `, [event.location, event.variable === 'temperature' ? 'temperature' : event.variable]);

          if (forecastRes.rows.length > 0) {
            forecastData = {
              values: forecastRes.rows.map(r => parseFloat(r.value_mean)),
              timestamps: forecastRes.rows.map(r => r.run_ts),
            };
          }
        }

        // Generate prediction
        const prediction = await generatePrediction({
          conditionId: market.condition_id,
          forecastFeatures: forecastData,
          eventSpec: event,
          month,
        });

        if (prediction.p_yes === null) continue;

        // Calculate edge vs market
        const marketMid = market.token_price ? parseFloat(market.token_price) : 0.50;
        const { bestEdge, bestSide } = calculateEdge(prediction.p_yes, marketMid);

        // Determine recommendation
        let recommendation = 'HOLD';
        if (bestEdge >= 0.03) recommendation = bestSide === 'YES' ? 'BUY_YES' : 'BUY_NO';
        else if (bestEdge < -0.05) recommendation = 'SKIP';

        // Store
        await storePrediction({
          conditionId: market.condition_id,
          asofTs: now,
          modelVersionId: 1,
          pYes: prediction.p_yes,
          confidence: prediction.confidence,
          edge: bestEdge,
          marketMid,
          recommendation,
          reasoning: { ...prediction.components, event_spec: event },
        });

        predictionsMade++;
      }

      await query(`INSERT INTO alerts_log (alert_type, severity, message, details) VALUES ($1, $2, $3, $4)`,
        ['predictions', 'info', `Generated ${predictionsMade} predictions`, { predictions_made: predictionsMade, markets_checked: marketsRes.rows.length }]);

      return NextResponse.json({ predictions_made: predictionsMade, markets_checked: marketsRes.rows.length });
    } catch (err) {
      return NextResponse.json({ error: err.message }, { status: 500 });
    }
  }

  return NextResponse.json({ error: 'Unknown action' }, { status: 400 });
}