feat(analysis): add monthly pattern analysis feature with chart and API integration
This commit is contained in:
122
apps/backend/src/modules/quotes/handlers/getMonthlyPattern.ts
Normal file
122
apps/backend/src/modules/quotes/handlers/getMonthlyPattern.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import type { Context } from "hono";
|
||||||
|
import { cache } from "../../../lib/cache";
|
||||||
|
import { prisma } from "../../../lib/prisma";
|
||||||
|
|
||||||
|
const VALID_TYPES = ["BLUE", "BNA", "BELO"] as const;
|
||||||
|
const ONE_HOUR = 60 * 60 * 1000;
|
||||||
|
|
||||||
|
export async function getMonthlyPattern(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 cacheKey = `quotes:monthly-pattern:${rawType}`;
|
||||||
|
const cached = cache.get(cacheKey);
|
||||||
|
if (cached) return c.json(cached);
|
||||||
|
|
||||||
|
const rows = await prisma.$queryRaw<
|
||||||
|
Array<{
|
||||||
|
day: number;
|
||||||
|
months: number;
|
||||||
|
avgBuy: number;
|
||||||
|
avgSell: number;
|
||||||
|
avgBuyPosition: number | null;
|
||||||
|
avgSellPosition: number | null;
|
||||||
|
minBuyDays: number;
|
||||||
|
maxBuyDays: number;
|
||||||
|
minSellDays: number;
|
||||||
|
maxSellDays: number;
|
||||||
|
}>
|
||||||
|
>`
|
||||||
|
WITH monthly AS (
|
||||||
|
SELECT
|
||||||
|
DATE_TRUNC('month', "timeStamp")::date AS month,
|
||||||
|
MIN(buy::numeric) AS min_buy,
|
||||||
|
MAX(buy::numeric) AS max_buy,
|
||||||
|
MIN(sell::numeric) AS min_sell,
|
||||||
|
MAX(sell::numeric) AS max_sell
|
||||||
|
FROM "quotes_history"
|
||||||
|
WHERE "type" = ${rawType}::"QuoteType"
|
||||||
|
AND "timeStamp" < DATE_TRUNC('month', NOW())
|
||||||
|
GROUP BY DATE_TRUNC('month', "timeStamp")
|
||||||
|
),
|
||||||
|
daily AS (
|
||||||
|
SELECT
|
||||||
|
DATE_TRUNC('month', "timeStamp")::date AS month,
|
||||||
|
EXTRACT(DAY FROM "timeStamp")::int AS day,
|
||||||
|
AVG(buy::numeric) AS avg_buy,
|
||||||
|
AVG(sell::numeric) AS avg_sell,
|
||||||
|
MIN(buy::numeric) AS day_min_buy,
|
||||||
|
MAX(buy::numeric) AS day_max_buy,
|
||||||
|
MIN(sell::numeric) AS day_min_sell,
|
||||||
|
MAX(sell::numeric) AS day_max_sell
|
||||||
|
FROM "quotes_history"
|
||||||
|
WHERE "type" = ${rawType}::"QuoteType"
|
||||||
|
AND "timeStamp" < DATE_TRUNC('month', NOW())
|
||||||
|
GROUP BY
|
||||||
|
DATE_TRUNC('month', "timeStamp"),
|
||||||
|
EXTRACT(DAY FROM "timeStamp")
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
d.day,
|
||||||
|
COUNT(*)::int AS months,
|
||||||
|
AVG(d.avg_buy)::float8 AS "avgBuy",
|
||||||
|
AVG(d.avg_sell)::float8 AS "avgSell",
|
||||||
|
AVG(
|
||||||
|
CASE WHEN m.max_buy > m.min_buy
|
||||||
|
THEN (d.avg_buy - m.min_buy) / (m.max_buy - m.min_buy)
|
||||||
|
END
|
||||||
|
)::float8 AS "avgBuyPosition",
|
||||||
|
AVG(
|
||||||
|
CASE WHEN m.max_sell > m.min_sell
|
||||||
|
THEN (d.avg_sell - m.min_sell) / (m.max_sell - m.min_sell)
|
||||||
|
END
|
||||||
|
)::float8 AS "avgSellPosition",
|
||||||
|
COUNT(*) FILTER (WHERE d.day_min_buy = m.min_buy)::int AS "minBuyDays",
|
||||||
|
COUNT(*) FILTER (WHERE d.day_max_buy = m.max_buy)::int AS "maxBuyDays",
|
||||||
|
COUNT(*) FILTER (WHERE d.day_min_sell = m.min_sell)::int AS "minSellDays",
|
||||||
|
COUNT(*) FILTER (WHERE d.day_max_sell = m.max_sell)::int AS "maxSellDays"
|
||||||
|
FROM daily d
|
||||||
|
JOIN monthly m ON m.month = d.month
|
||||||
|
GROUP BY d.day
|
||||||
|
ORDER BY d.day ASC
|
||||||
|
`;
|
||||||
|
|
||||||
|
const totalMonths = await prisma.$queryRaw<Array<{ count: number }>>`
|
||||||
|
SELECT COUNT(*)::int AS count
|
||||||
|
FROM (
|
||||||
|
SELECT DATE_TRUNC('month', "timeStamp")::date AS month
|
||||||
|
FROM "quotes_history"
|
||||||
|
WHERE "type" = ${rawType}::"QuoteType"
|
||||||
|
AND "timeStamp" < DATE_TRUNC('month', NOW())
|
||||||
|
GROUP BY DATE_TRUNC('month', "timeStamp")
|
||||||
|
) AS months
|
||||||
|
`;
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
type: rawType,
|
||||||
|
totalMonths: totalMonths[0]?.count ?? 0,
|
||||||
|
days: rows.map((r) => ({
|
||||||
|
day: r.day,
|
||||||
|
months: r.months,
|
||||||
|
avgBuy: r.avgBuy,
|
||||||
|
avgSell: r.avgSell,
|
||||||
|
avgBuyPosition: r.avgBuyPosition,
|
||||||
|
avgSellPosition: r.avgSellPosition,
|
||||||
|
minBuyDays: r.minBuyDays,
|
||||||
|
maxBuyDays: r.maxBuyDays,
|
||||||
|
minSellDays: r.minSellDays,
|
||||||
|
maxSellDays: r.maxSellDays,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
cache.set(cacheKey, response, ONE_HOUR);
|
||||||
|
return c.json(response);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { getDailyMinMax } from "./handlers/getDailyMinMax";
|
|||||||
import { getDailyQuotes } from "./handlers/getDailyQuotes";
|
import { getDailyQuotes } from "./handlers/getDailyQuotes";
|
||||||
import { getHistoricalMinMax } from "./handlers/getHistoricalMinMax";
|
import { getHistoricalMinMax } from "./handlers/getHistoricalMinMax";
|
||||||
import { getMonthlyLast } from "./handlers/getMonthlyLast";
|
import { getMonthlyLast } from "./handlers/getMonthlyLast";
|
||||||
|
import { getMonthlyPattern } from "./handlers/getMonthlyPattern";
|
||||||
import { getPeakHours } from "./handlers/getPeakHours";
|
import { getPeakHours } from "./handlers/getPeakHours";
|
||||||
import { getQuoteHistory } from "./handlers/getQuoteHistory";
|
import { getQuoteHistory } from "./handlers/getQuoteHistory";
|
||||||
import { quoteEvents } from "./handlers/quoteEvents";
|
import { quoteEvents } from "./handlers/quoteEvents";
|
||||||
@@ -17,6 +18,7 @@ app.get("/:type/history", getQuoteHistory);
|
|||||||
app.get("/:type/daily", getDailyQuotes);
|
app.get("/:type/daily", getDailyQuotes);
|
||||||
app.get("/:type/min-max", getDailyMinMax);
|
app.get("/:type/min-max", getDailyMinMax);
|
||||||
app.get("/:type/peak-hours", getPeakHours);
|
app.get("/:type/peak-hours", getPeakHours);
|
||||||
|
app.get("/:type/monthly-pattern", getMonthlyPattern);
|
||||||
app.get("/:type/historical/min-max", getHistoricalMinMax);
|
app.get("/:type/historical/min-max", getHistoricalMinMax);
|
||||||
app.get("/fetch", fetchQuotes);
|
app.get("/fetch", fetchQuotes);
|
||||||
app.get("/events", quoteEvents);
|
app.get("/events", quoteEvents);
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ import {
|
|||||||
useDailyQuotes,
|
useDailyQuotes,
|
||||||
useHistoricalMinMax,
|
useHistoricalMinMax,
|
||||||
useMonthlyLast,
|
useMonthlyLast,
|
||||||
|
useMonthlyPattern,
|
||||||
usePeakHours,
|
usePeakHours,
|
||||||
useQuotes,
|
useQuotes,
|
||||||
} from "@/lib/queries";
|
} from "@/lib/queries";
|
||||||
import { MonthlyVariationTable } from "./components/MonthlyVariationTable";
|
import { MonthlyVariationTable } from "./components/MonthlyVariationTable";
|
||||||
|
import { MonthPatternChart } from "./components/MonthPatternChart";
|
||||||
import { PeakHoursChart } from "./components/PeakHoursChart";
|
import { PeakHoursChart } from "./components/PeakHoursChart";
|
||||||
|
|
||||||
const CURRENCIES = ["BELO", "BLUE", "BNA"] as const;
|
const CURRENCIES = ["BELO", "BLUE", "BNA"] as const;
|
||||||
@@ -104,6 +106,9 @@ export function AnalysisPage({ currency }: { currency: string }) {
|
|||||||
const { data: peakHoursData, isLoading: peakHoursLoading } =
|
const { data: peakHoursData, isLoading: peakHoursLoading } =
|
||||||
usePeakHours("BELO");
|
usePeakHours("BELO");
|
||||||
|
|
||||||
|
const { data: monthlyPatternData, isLoading: monthlyPatternLoading } =
|
||||||
|
useMonthlyPattern("BELO");
|
||||||
|
|
||||||
const chartData = useMemo(() => {
|
const chartData = useMemo(() => {
|
||||||
if (!daily) return [];
|
if (!daily) return [];
|
||||||
return daily.map((d) => ({
|
return daily.map((d) => ({
|
||||||
@@ -598,16 +603,31 @@ export function AnalysisPage({ currency }: { currency: string }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{currency === "BELO" && (
|
{currency === "BELO" && (
|
||||||
<div className="rounded-lg border p-4">
|
<>
|
||||||
<h2 className="text-sm font-medium mb-4">
|
<div className="rounded-lg border p-4">
|
||||||
Análisis de pico horario
|
<h2 className="text-sm font-medium mb-4">
|
||||||
</h2>
|
Análisis de pico horario
|
||||||
<p className="text-xs text-muted-foreground mb-4">
|
</h2>
|
||||||
Frecuencia con la que cada hora del día fue el momento de mayor
|
<p className="text-xs text-muted-foreground mb-4">
|
||||||
precio de compra dentro de cada día.
|
Frecuencia con la que cada hora del día fue el momento de mayor
|
||||||
</p>
|
precio de compra dentro de cada día.
|
||||||
<PeakHoursChart data={peakHoursData} isLoading={peakHoursLoading} />
|
</p>
|
||||||
</div>
|
<PeakHoursChart data={peakHoursData} isLoading={peakHoursLoading} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg border p-4">
|
||||||
|
<h2 className="text-sm font-medium mb-4">Patrón mensual</h2>
|
||||||
|
<p className="text-xs text-muted-foreground mb-4">
|
||||||
|
Posición del precio dentro de cada mes (0% = mínimo del mes, 100%
|
||||||
|
= máximo), promediada entre los meses completos disponibles. Se
|
||||||
|
excluye la tendencia inflacionaria para comparar momentos del mes.
|
||||||
|
</p>
|
||||||
|
<MonthPatternChart
|
||||||
|
data={monthlyPatternData}
|
||||||
|
isLoading={monthlyPatternLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,293 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
CartesianGrid,
|
||||||
|
Cell,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from "recharts";
|
||||||
|
import type { MonthlyPatternResponse } from "@/lib/api";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const priceFormatter = new Intl.NumberFormat("es-AR", {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
type Mode = "sell" | "buy";
|
||||||
|
|
||||||
|
type ChartEntry = {
|
||||||
|
day: number;
|
||||||
|
months: number;
|
||||||
|
position: number;
|
||||||
|
avgPrice: number;
|
||||||
|
recommended: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function MonthPatternChart({
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
}: {
|
||||||
|
data: MonthlyPatternResponse | undefined;
|
||||||
|
isLoading: boolean;
|
||||||
|
}) {
|
||||||
|
const [mode, setMode] = useState<Mode>("sell");
|
||||||
|
|
||||||
|
const { chartData, totalMonths, bestSellDay, bestBuyDay, minCoverage } =
|
||||||
|
useMemo(() => {
|
||||||
|
const totalMonths = data?.totalMonths ?? 0;
|
||||||
|
const allDays = data?.days ?? [];
|
||||||
|
const byDay = new Map(allDays.map((d) => [d.day, d]));
|
||||||
|
const minCoverage = Math.max(3, Math.ceil(totalMonths * 0.5));
|
||||||
|
|
||||||
|
const eligible = allDays.filter((d) => d.months >= minCoverage);
|
||||||
|
|
||||||
|
let bestSellDay: number | null = null;
|
||||||
|
let bestSellPosition = -Infinity;
|
||||||
|
for (const d of eligible) {
|
||||||
|
const pos = d.avgBuyPosition ?? null;
|
||||||
|
if (pos !== null && pos > bestSellPosition) {
|
||||||
|
bestSellPosition = pos;
|
||||||
|
bestSellDay = d.day;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bestBuyDay: number | null = null;
|
||||||
|
let bestBuyPosition = Infinity;
|
||||||
|
for (const d of eligible) {
|
||||||
|
const pos = d.avgSellPosition ?? null;
|
||||||
|
if (pos !== null && pos < bestBuyPosition) {
|
||||||
|
bestBuyPosition = pos;
|
||||||
|
bestBuyDay = d.day;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const recommended = mode === "sell" ? bestSellDay : bestBuyDay;
|
||||||
|
|
||||||
|
const chartData: ChartEntry[] = Array.from({ length: 31 }, (_, i) => {
|
||||||
|
const day = i + 1;
|
||||||
|
const found = byDay.get(day);
|
||||||
|
const position =
|
||||||
|
mode === "sell"
|
||||||
|
? (found?.avgBuyPosition ?? 0)
|
||||||
|
: (found?.avgSellPosition ?? 0);
|
||||||
|
return {
|
||||||
|
day,
|
||||||
|
months: found?.months ?? 0,
|
||||||
|
position,
|
||||||
|
avgPrice:
|
||||||
|
mode === "sell" ? (found?.avgBuy ?? 0) : (found?.avgSell ?? 0),
|
||||||
|
recommended: recommended !== null && day === recommended,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
chartData,
|
||||||
|
totalMonths,
|
||||||
|
bestSellDay,
|
||||||
|
bestBuyDay,
|
||||||
|
minCoverage,
|
||||||
|
};
|
||||||
|
}, [data, mode]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-64 text-muted-foreground text-sm">
|
||||||
|
Cargando datos del patrón mensual...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalMonths === 0 || chartData.every((d) => d.months === 0)) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-64 text-muted-foreground text-sm">
|
||||||
|
Sin suficiente historial. Se necesitan al menos 2 meses completos de
|
||||||
|
datos.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid gap-4 md:grid-cols-3">
|
||||||
|
<div className="rounded-lg border p-4">
|
||||||
|
<p className="text-sm text-muted-foreground mb-1">
|
||||||
|
Mejor día para vender USDC
|
||||||
|
</p>
|
||||||
|
<p className="text-2xl font-bold">
|
||||||
|
{bestSellDay !== null ? `Día ${bestSellDay}` : "Sin datos"}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
Precio de compra más alto del mes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border p-4">
|
||||||
|
<p className="text-sm text-muted-foreground mb-1">
|
||||||
|
Mejor día para comprar USDC
|
||||||
|
</p>
|
||||||
|
<p className="text-2xl font-bold">
|
||||||
|
{bestBuyDay !== null ? `Día ${bestBuyDay}` : "Sin datos"}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
Precio de venta más bajo del mes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border p-4">
|
||||||
|
<p className="text-sm text-muted-foreground mb-1">Meses analizados</p>
|
||||||
|
<p className="text-2xl font-bold tabular-nums">{totalMonths}</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
Mínimo de cobertura por día: {minCoverage} meses
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex items-center gap-1 rounded-lg border p-0.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setMode("sell")}
|
||||||
|
className={cn(
|
||||||
|
"px-3 py-1 text-xs rounded-md transition-colors cursor-pointer",
|
||||||
|
mode === "sell"
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "text-muted-foreground hover:text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Vender USDC (compra)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setMode("buy")}
|
||||||
|
className={cn(
|
||||||
|
"px-3 py-1 text-xs rounded-md transition-colors cursor-pointer",
|
||||||
|
mode === "buy"
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "text-muted-foreground hover:text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Comprar USDC (venta)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Posición promedio dentro del mes (0% = mínimo, 100% = máximo)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-64">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart data={chartData}>
|
||||||
|
<CartesianGrid
|
||||||
|
strokeDasharray="3 3"
|
||||||
|
className="stroke-border"
|
||||||
|
vertical={false}
|
||||||
|
/>
|
||||||
|
<XAxis
|
||||||
|
dataKey="day"
|
||||||
|
tick={{ fontSize: 10 }}
|
||||||
|
className="text-muted-foreground"
|
||||||
|
interval={2}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
tick={{ fontSize: 11 }}
|
||||||
|
className="text-muted-foreground"
|
||||||
|
domain={[0, 1]}
|
||||||
|
tickFormatter={(v: number) => `${Math.round(v * 100)}%`}
|
||||||
|
label={{
|
||||||
|
value: "Posición en el mes",
|
||||||
|
angle: -90,
|
||||||
|
position: "insideLeft",
|
||||||
|
offset: 10,
|
||||||
|
style: { fontSize: 11, fill: "hsl(var(--muted-foreground))" },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
cursor={{ fill: "hsl(var(--muted) / 0.3)" }}
|
||||||
|
content={({ active, payload }) => {
|
||||||
|
if (!active || !payload?.length) return null;
|
||||||
|
const d = payload[0].payload as ChartEntry;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="rounded-lg border bg-card p-3 text-sm shadow-sm"
|
||||||
|
style={{ minWidth: 180 }}
|
||||||
|
>
|
||||||
|
<p className="font-semibold mb-1">Día {d.day}</p>
|
||||||
|
<div className="space-y-0.5 text-muted-foreground">
|
||||||
|
<p>
|
||||||
|
Posición:{" "}
|
||||||
|
<span className="font-medium text-foreground">
|
||||||
|
{Math.round(d.position * 100)}%
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
{d.avgPrice > 0 && (
|
||||||
|
<p>
|
||||||
|
Precio promedio:{" "}
|
||||||
|
<span className="font-medium text-foreground">
|
||||||
|
${priceFormatter.format(d.avgPrice)}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p>
|
||||||
|
Con datos en:{" "}
|
||||||
|
<span className="font-medium text-foreground">
|
||||||
|
{d.months} meses
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
{d.recommended && (
|
||||||
|
<p className="text-emerald-500 font-medium">
|
||||||
|
Día recomendado
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
dataKey="position"
|
||||||
|
radius={[4, 4, 0, 0]}
|
||||||
|
isAnimationActive={false}
|
||||||
|
>
|
||||||
|
{chartData.map((entry) => (
|
||||||
|
<Cell
|
||||||
|
key={entry.day}
|
||||||
|
fill={
|
||||||
|
entry.recommended
|
||||||
|
? mode === "sell"
|
||||||
|
? "var(--chart-1)"
|
||||||
|
: "var(--chart-2)"
|
||||||
|
: "hsl(var(--muted-foreground) / 0.25)"
|
||||||
|
}
|
||||||
|
fillOpacity={entry.months === 0 ? 0.4 : 1}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Bar>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center gap-4">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span
|
||||||
|
className="size-2.5 rounded-full"
|
||||||
|
style={{ backgroundColor: "var(--chart-1)" }}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Mejor día para vender USDC
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span
|
||||||
|
className="size-2.5 rounded-full"
|
||||||
|
style={{ backgroundColor: "var(--chart-2)" }}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Mejor día para comprar USDC
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -58,6 +58,25 @@ export type PeakHoursResponse = {
|
|||||||
peaks: PeakHour[];
|
peaks: PeakHour[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MonthPatternDay = {
|
||||||
|
day: number;
|
||||||
|
months: number;
|
||||||
|
avgBuy: number;
|
||||||
|
avgSell: number;
|
||||||
|
avgBuyPosition: number | null;
|
||||||
|
avgSellPosition: number | null;
|
||||||
|
minBuyDays: number;
|
||||||
|
maxBuyDays: number;
|
||||||
|
minSellDays: number;
|
||||||
|
maxSellDays: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MonthlyPatternResponse = {
|
||||||
|
type: string;
|
||||||
|
totalMonths: number;
|
||||||
|
days: MonthPatternDay[];
|
||||||
|
};
|
||||||
|
|
||||||
async function fetcher<T>(url: string): Promise<T> {
|
async function fetcher<T>(url: string): Promise<T> {
|
||||||
const res = await fetch(url, { credentials: "include" });
|
const res = await fetch(url, { credentials: "include" });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -126,6 +145,12 @@ export function getPeakHours(type: string): Promise<PeakHoursResponse> {
|
|||||||
return fetcher<PeakHoursResponse>(`/api/quotes/${type}/peak-hours`);
|
return fetcher<PeakHoursResponse>(`/api/quotes/${type}/peak-hours`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMonthlyPattern(
|
||||||
|
type: string,
|
||||||
|
): Promise<MonthlyPatternResponse> {
|
||||||
|
return fetcher<MonthlyPatternResponse>(`/api/quotes/${type}/monthly-pattern`);
|
||||||
|
}
|
||||||
|
|
||||||
export type MonthlyLast = {
|
export type MonthlyLast = {
|
||||||
month: string;
|
month: string;
|
||||||
type: "BELO" | "BLUE" | "BNA";
|
type: "BELO" | "BLUE" | "BNA";
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
getHistoricalMinMax,
|
getHistoricalMinMax,
|
||||||
getInflation,
|
getInflation,
|
||||||
getMonthlyLast,
|
getMonthlyLast,
|
||||||
|
getMonthlyPattern,
|
||||||
getMonthlyPayedTotal,
|
getMonthlyPayedTotal,
|
||||||
getMonthlyTotals,
|
getMonthlyTotals,
|
||||||
getPeakHours,
|
getPeakHours,
|
||||||
@@ -64,8 +65,8 @@ export const quoteKeys = {
|
|||||||
["quotes", type, "daily", startDate, endDate] as const,
|
["quotes", type, "daily", startDate, endDate] as const,
|
||||||
historicalMinMax: (type: string) =>
|
historicalMinMax: (type: string) =>
|
||||||
["quotes", type, "historicalMinMax"] as const,
|
["quotes", type, "historicalMinMax"] as const,
|
||||||
peakHours: (type: string) =>
|
peakHours: (type: string) => ["quotes", type, "peakHours"] as const,
|
||||||
["quotes", type, "peakHours"] as const,
|
monthlyPattern: (type: string) => ["quotes", type, "monthlyPattern"] as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useQuotes() {
|
export function useQuotes() {
|
||||||
@@ -143,6 +144,14 @@ export function usePeakHours(type: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useMonthlyPattern(type: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: quoteKeys.monthlyPattern(type),
|
||||||
|
queryFn: () => getMonthlyPattern(type),
|
||||||
|
staleTime: 30_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useMonthlyLast() {
|
export function useMonthlyLast() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["quotes", "monthlyLast"] as const,
|
queryKey: ["quotes", "monthlyLast"] as const,
|
||||||
|
|||||||
311
pnpm-lock.yaml
generated
Normal file
311
pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
importers:
|
||||||
|
|
||||||
|
.:
|
||||||
|
dependencies:
|
||||||
|
'@noble/ciphers':
|
||||||
|
specifier: ^2.2.0
|
||||||
|
version: 2.4.0
|
||||||
|
devDependencies:
|
||||||
|
'@biomejs/biome':
|
||||||
|
specifier: ^2.4.16
|
||||||
|
version: 2.5.10
|
||||||
|
concurrently:
|
||||||
|
specifier: ^9.1.0
|
||||||
|
version: 9.2.4
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
'@biomejs/biome@2.5.10':
|
||||||
|
resolution: {integrity: sha512-WRKXARA3kTuiV5sxqTpobJ/I0MVd4vk3pOL6wnp5az4LntFIhWTj1RWZq3DI9PCEN3lXcqy7p5aqUHzvq8AXyQ==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
'@biomejs/cli-darwin-arm64@2.5.10':
|
||||||
|
resolution: {integrity: sha512-ItCrxKK6SXVT6flYs0qIuBd4AA3TTTl4d66Re6YI2FuGZnN85NmuYNzkiTJUyYw8qBLv69L5zTUB6uyWd++h3Q==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@biomejs/cli-darwin-x64@2.5.10':
|
||||||
|
resolution: {integrity: sha512-yLsPU9pAmtChXDu8vhKAzErqe+LeeYuwuUB2FZMkRitsmdodxsYRa9KHrFispsUHzzOu+9HB3nP/TQxyia+Sjw==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@biomejs/cli-linux-arm64-musl@2.5.10':
|
||||||
|
resolution: {integrity: sha512-t1QAKZwQJRB4dvgJSgFiQ4BNfNPChg69BNonz854qLVxnjT3UvDzQg9mbkTJRu35ZqU0Rw10A73J8Urgbg2RPw==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@biomejs/cli-linux-arm64@2.5.10':
|
||||||
|
resolution: {integrity: sha512-VG8uQW/86a1roLaIFvtIbEigxIdzdJ190oGyg1tV7VYeQtOS+x10sflk7WbuXgw91EtZX5DlIIIej1YqkNLlcg==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@biomejs/cli-linux-x64-musl@2.5.10':
|
||||||
|
resolution: {integrity: sha512-pgDDqp9JybHm2I0KRgzN6i4+lt8xu4iqxUwLzglUMmOmyRTU1AYBGKzh9sNMOtIjah7xoWvKHlLVetvyifzoiQ==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@biomejs/cli-linux-x64@2.5.10':
|
||||||
|
resolution: {integrity: sha512-4O6T0eq2heoHZN0a9UX+rWQoxXEBaKf+lRi2hbsGlHneUz9BWXM76nEWMK7Eeq8gzMxR1khQB6BFpAASpeXqGg==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@biomejs/cli-win32-arm64@2.5.10':
|
||||||
|
resolution: {integrity: sha512-pxAbxduPO4xq/Cvgaa2lOrs9BB0hEXmmDqfMNP4ZOffGOkUrD1/QGw9UAMpFQpX2P8MqTIIRuQKcmetum4Oa6A==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@biomejs/cli-win32-x64@2.5.10':
|
||||||
|
resolution: {integrity: sha512-M+2dgBsl3lXRiTfgPVc2p3anS4Tocojke4rzFLScZ2Y/wmF+36dRb1iHCLiyGqOzQGyTplZH1HnEYviiAqi3nA==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@noble/ciphers@2.4.0':
|
||||||
|
resolution: {integrity: sha512-AnjFn0Jv92laAkvMrghlFZq4qQCIN/4DxFV/eooqtC2YTjB7kBeLMS2T9KJX4Dn+ZVXLOwK0lSgqDtx9gvxtiw==}
|
||||||
|
engines: {node: '>= 20.19.0'}
|
||||||
|
|
||||||
|
ansi-regex@5.0.1:
|
||||||
|
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
ansi-styles@4.3.0:
|
||||||
|
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
chalk@4.1.2:
|
||||||
|
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
cliui@8.0.1:
|
||||||
|
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
color-convert@2.0.1:
|
||||||
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
|
engines: {node: '>=7.0.0'}
|
||||||
|
|
||||||
|
color-name@1.1.4:
|
||||||
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
|
concurrently@9.2.4:
|
||||||
|
resolution: {integrity: sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
emoji-regex@8.0.0:
|
||||||
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
|
|
||||||
|
escalade@3.2.0:
|
||||||
|
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
get-caller-file@2.0.5:
|
||||||
|
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||||
|
engines: {node: 6.* || 8.* || >= 10.*}
|
||||||
|
|
||||||
|
has-flag@4.0.0:
|
||||||
|
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
is-fullwidth-code-point@3.0.0:
|
||||||
|
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
require-directory@2.1.1:
|
||||||
|
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
rxjs@7.8.2:
|
||||||
|
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
||||||
|
|
||||||
|
shell-quote@1.9.0:
|
||||||
|
resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
string-width@4.2.3:
|
||||||
|
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
strip-ansi@6.0.1:
|
||||||
|
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
supports-color@7.2.0:
|
||||||
|
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
supports-color@8.1.1:
|
||||||
|
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
tree-kill@1.2.2:
|
||||||
|
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
tslib@2.8.1:
|
||||||
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
|
wrap-ansi@7.0.0:
|
||||||
|
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
y18n@5.0.8:
|
||||||
|
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
yargs-parser@21.1.1:
|
||||||
|
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
yargs@17.7.2:
|
||||||
|
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
snapshots:
|
||||||
|
|
||||||
|
'@biomejs/biome@2.5.10':
|
||||||
|
optionalDependencies:
|
||||||
|
'@biomejs/cli-darwin-arm64': 2.5.10
|
||||||
|
'@biomejs/cli-darwin-x64': 2.5.10
|
||||||
|
'@biomejs/cli-linux-arm64': 2.5.10
|
||||||
|
'@biomejs/cli-linux-arm64-musl': 2.5.10
|
||||||
|
'@biomejs/cli-linux-x64': 2.5.10
|
||||||
|
'@biomejs/cli-linux-x64-musl': 2.5.10
|
||||||
|
'@biomejs/cli-win32-arm64': 2.5.10
|
||||||
|
'@biomejs/cli-win32-x64': 2.5.10
|
||||||
|
|
||||||
|
'@biomejs/cli-darwin-arm64@2.5.10':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@biomejs/cli-darwin-x64@2.5.10':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@biomejs/cli-linux-arm64-musl@2.5.10':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@biomejs/cli-linux-arm64@2.5.10':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@biomejs/cli-linux-x64-musl@2.5.10':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@biomejs/cli-linux-x64@2.5.10':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@biomejs/cli-win32-arm64@2.5.10':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@biomejs/cli-win32-x64@2.5.10':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@noble/ciphers@2.4.0': {}
|
||||||
|
|
||||||
|
ansi-regex@5.0.1: {}
|
||||||
|
|
||||||
|
ansi-styles@4.3.0:
|
||||||
|
dependencies:
|
||||||
|
color-convert: 2.0.1
|
||||||
|
|
||||||
|
chalk@4.1.2:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
supports-color: 7.2.0
|
||||||
|
|
||||||
|
cliui@8.0.1:
|
||||||
|
dependencies:
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
wrap-ansi: 7.0.0
|
||||||
|
|
||||||
|
color-convert@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
color-name: 1.1.4
|
||||||
|
|
||||||
|
color-name@1.1.4: {}
|
||||||
|
|
||||||
|
concurrently@9.2.4:
|
||||||
|
dependencies:
|
||||||
|
chalk: 4.1.2
|
||||||
|
rxjs: 7.8.2
|
||||||
|
shell-quote: 1.9.0
|
||||||
|
supports-color: 8.1.1
|
||||||
|
tree-kill: 1.2.2
|
||||||
|
yargs: 17.7.2
|
||||||
|
|
||||||
|
emoji-regex@8.0.0: {}
|
||||||
|
|
||||||
|
escalade@3.2.0: {}
|
||||||
|
|
||||||
|
get-caller-file@2.0.5: {}
|
||||||
|
|
||||||
|
has-flag@4.0.0: {}
|
||||||
|
|
||||||
|
is-fullwidth-code-point@3.0.0: {}
|
||||||
|
|
||||||
|
require-directory@2.1.1: {}
|
||||||
|
|
||||||
|
rxjs@7.8.2:
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
shell-quote@1.9.0: {}
|
||||||
|
|
||||||
|
string-width@4.2.3:
|
||||||
|
dependencies:
|
||||||
|
emoji-regex: 8.0.0
|
||||||
|
is-fullwidth-code-point: 3.0.0
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
|
strip-ansi@6.0.1:
|
||||||
|
dependencies:
|
||||||
|
ansi-regex: 5.0.1
|
||||||
|
|
||||||
|
supports-color@7.2.0:
|
||||||
|
dependencies:
|
||||||
|
has-flag: 4.0.0
|
||||||
|
|
||||||
|
supports-color@8.1.1:
|
||||||
|
dependencies:
|
||||||
|
has-flag: 4.0.0
|
||||||
|
|
||||||
|
tree-kill@1.2.2: {}
|
||||||
|
|
||||||
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
|
wrap-ansi@7.0.0:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
|
y18n@5.0.8: {}
|
||||||
|
|
||||||
|
yargs-parser@21.1.1: {}
|
||||||
|
|
||||||
|
yargs@17.7.2:
|
||||||
|
dependencies:
|
||||||
|
cliui: 8.0.1
|
||||||
|
escalade: 3.2.0
|
||||||
|
get-caller-file: 2.0.5
|
||||||
|
require-directory: 2.1.1
|
||||||
|
string-width: 4.2.3
|
||||||
|
y18n: 5.0.8
|
||||||
|
yargs-parser: 21.1.1
|
||||||
Reference in New Issue
Block a user