71 lines
2.5 KiB
TypeScript
71 lines
2.5 KiB
TypeScript
import type { APIRoute } from 'astro';
|
|
import { db } from '../../db/index';
|
|
import { priceHistory, skus } from '../../db/schema';
|
|
import { eq, inArray } from 'drizzle-orm';
|
|
|
|
export const prerender = false;
|
|
|
|
export const GET: APIRoute = async ({ url }) => {
|
|
const cardId = Number(url.searchParams.get('cardId')) || 0;
|
|
|
|
const cardSkus = await db
|
|
.select()
|
|
.from(skus)
|
|
.where(eq(skus.cardId, cardId));
|
|
|
|
if (!cardSkus.length) {
|
|
return new Response(JSON.stringify([]), { headers: { 'Content-Type': 'application/json' } });
|
|
}
|
|
|
|
const skuIds = cardSkus.map(s => s.skuId);
|
|
|
|
const historyRows = await db
|
|
.select({
|
|
skuId: priceHistory.skuId,
|
|
calculatedAt: priceHistory.calculatedAt,
|
|
marketPrice: priceHistory.marketPrice,
|
|
condition: skus.condition,
|
|
})
|
|
.from(priceHistory)
|
|
.innerJoin(skus, eq(priceHistory.skuId, skus.skuId))
|
|
.where(inArray(priceHistory.skuId, skuIds))
|
|
.orderBy(priceHistory.calculatedAt);
|
|
|
|
// Rolling 30-day cutoff for volatility
|
|
const thirtyDaysAgo = new Date(Date.now() - 30 * 86_400_000);
|
|
|
|
const byCondition: Record<string, number[]> = {};
|
|
for (const row of historyRows) {
|
|
if (row.marketPrice == null) continue;
|
|
if (!row.calculatedAt) continue;
|
|
if (new Date(row.calculatedAt) < thirtyDaysAgo) continue;
|
|
const price = Number(row.marketPrice);
|
|
if (price <= 0) continue;
|
|
if (!byCondition[row.condition]) byCondition[row.condition] = [];
|
|
byCondition[row.condition].push(price);
|
|
}
|
|
|
|
function computeVolatility(prices: number[]): { label: string; monthlyVol: number } {
|
|
if (prices.length < 2) return { label: '—', monthlyVol: 0 };
|
|
const returns: number[] = [];
|
|
for (let i = 1; i < prices.length; i++) {
|
|
returns.push(Math.log(prices[i] / prices[i - 1]));
|
|
}
|
|
const mean = returns.reduce((a, b) => a + b, 0) / returns.length;
|
|
const variance = returns.reduce((sum, r) => sum + (r - mean) ** 2, 0) / (returns.length - 1);
|
|
const monthlyVol = Math.sqrt(variance) * Math.sqrt(30);
|
|
const label = monthlyVol >= 0.30 ? 'High'
|
|
: monthlyVol >= 0.15 ? 'Medium'
|
|
: 'Low';
|
|
return { label, monthlyVol: Math.round(monthlyVol * 100) / 100 };
|
|
}
|
|
|
|
const volatilityByCondition: Record<string, { label: string; monthlyVol: number }> = {};
|
|
for (const [condition, prices] of Object.entries(byCondition)) {
|
|
volatilityByCondition[condition] = computeVolatility(prices);
|
|
}
|
|
|
|
return new Response(JSON.stringify({ history: historyRows, volatilityByCondition }), {
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
}; |