← 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 });
}