feat: add BELO peak hour analysis chart
- New backend endpoint GET /quotes/:type/peak-hours - Groups daily price data by hour to find peak frequency - Bar chart showing how often each hour is the daily peak - Only displayed for BELO currency in analysis page
This commit is contained in:
95
apps/backend/src/modules/quotes/handlers/getPeakHours.ts
Normal file
95
apps/backend/src/modules/quotes/handlers/getPeakHours.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { Context } from "hono";
|
||||
import type { QuoteType } from "../../../generated/prisma/client";
|
||||
import { cache } from "../../../lib/cache";
|
||||
import { prisma } from "../../../lib/prisma";
|
||||
import { parseLocalDate, startOfNextLocalDay } from "../../../lib/utils";
|
||||
|
||||
const VALID_TYPES = ["BLUE", "BNA", "BELO"] as const;
|
||||
|
||||
export async function getPeakHours(c: Context) {
|
||||
const rawType = c.req.param("type")?.toUpperCase();
|
||||
|
||||
if (!VALID_TYPES.includes(rawType as (typeof VALID_TYPES)[number])) {
|
||||
return c.json(
|
||||
{
|
||||
error: `Invalid quote type. Must be one of: ${VALID_TYPES.join(", ")}`,
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
const type = rawType as QuoteType;
|
||||
const startDateParam = c.req.query("startDate");
|
||||
const endDateParam = c.req.query("endDate");
|
||||
|
||||
const now = new Date();
|
||||
const startDate = startDateParam
|
||||
? parseLocalDate(startDateParam)
|
||||
: new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const endDate = endDateParam ? startOfNextLocalDay(endDateParam) : now;
|
||||
|
||||
if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) {
|
||||
return c.json({ error: "Invalid date format. Use ISO 8601." }, 400);
|
||||
}
|
||||
|
||||
const cacheKey = `quotes:peak-hours:${type}:${startDate.toISOString()}:${endDate.toISOString()}`;
|
||||
const cached = cache.get(cacheKey);
|
||||
if (cached) return c.json(cached);
|
||||
|
||||
const result = await prisma.$queryRaw<
|
||||
Array<{
|
||||
hour: number;
|
||||
frequency: number;
|
||||
avg_peak_buy: number;
|
||||
min_peak_buy: number;
|
||||
max_peak_buy: number;
|
||||
}>
|
||||
>`
|
||||
WITH daily_peaks AS (
|
||||
SELECT
|
||||
DATE("timeStamp") AS day,
|
||||
EXTRACT(HOUR FROM "timeStamp")::int AS peak_hour,
|
||||
buy::numeric AS peak_buy,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY DATE("timeStamp")
|
||||
ORDER BY buy::numeric DESC
|
||||
) AS rn
|
||||
FROM "quotes_history"
|
||||
WHERE "type" = ${rawType}::"QuoteType"
|
||||
AND "timeStamp" >= ${startDate}::timestamptz
|
||||
AND "timeStamp" < ${endDate}::timestamptz
|
||||
)
|
||||
SELECT
|
||||
peak_hour AS hour,
|
||||
COUNT(*)::int AS frequency,
|
||||
AVG(peak_buy)::float8 AS avg_peak_buy,
|
||||
MIN(peak_buy)::float8 AS min_peak_buy,
|
||||
MAX(peak_buy)::float8 AS max_peak_buy
|
||||
FROM daily_peaks
|
||||
WHERE rn = 1
|
||||
GROUP BY peak_hour
|
||||
ORDER BY peak_hour ASC
|
||||
`;
|
||||
|
||||
const totalDays = await prisma.$queryRaw<Array<{ count: number }>>`
|
||||
SELECT COUNT(DISTINCT DATE("timeStamp"))::int AS count
|
||||
FROM "quotes_history"
|
||||
WHERE "type" = ${rawType}::"QuoteType"
|
||||
AND "timeStamp" >= ${startDate}::timestamptz
|
||||
AND "timeStamp" < ${endDate}::timestamptz
|
||||
`;
|
||||
|
||||
const response = {
|
||||
totalDays: totalDays[0]?.count ?? 0,
|
||||
peaks: result.map((r) => ({
|
||||
hour: r.hour,
|
||||
frequency: r.frequency,
|
||||
avgPeakBuy: r.avg_peak_buy,
|
||||
minPeakBuy: r.min_peak_buy,
|
||||
maxPeakBuy: r.max_peak_buy,
|
||||
})),
|
||||
};
|
||||
|
||||
cache.set(cacheKey, response);
|
||||
return c.json(response);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { getDailyMinMax } from "./handlers/getDailyMinMax";
|
||||
import { getDailyQuotes } from "./handlers/getDailyQuotes";
|
||||
import { getHistoricalMinMax } from "./handlers/getHistoricalMinMax";
|
||||
import { getMonthlyLast } from "./handlers/getMonthlyLast";
|
||||
import { getPeakHours } from "./handlers/getPeakHours";
|
||||
import { getQuoteHistory } from "./handlers/getQuoteHistory";
|
||||
import { quoteEvents } from "./handlers/quoteEvents";
|
||||
|
||||
@@ -15,6 +16,7 @@ app.get("/monthly-last", getMonthlyLast);
|
||||
app.get("/:type/history", getQuoteHistory);
|
||||
app.get("/:type/daily", getDailyQuotes);
|
||||
app.get("/:type/min-max", getDailyMinMax);
|
||||
app.get("/:type/peak-hours", getPeakHours);
|
||||
app.get("/:type/historical/min-max", getHistoricalMinMax);
|
||||
app.get("/fetch", fetchQuotes);
|
||||
app.get("/events", quoteEvents);
|
||||
|
||||
Reference in New Issue
Block a user