This commit is contained in:
@@ -41,8 +41,9 @@ import { useState, useEffect } from "react";
|
|||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { EditUserModal } from "./edit-user-modal";
|
import { EditUserModal } from "./edit-user-modal";
|
||||||
import LeagueTiersContent from "./league-tiers-content";
|
import LeagueTiersContent from "./league-tiers-content";
|
||||||
|
import ModelPerformanceContent from "./model-performance-content";
|
||||||
|
|
||||||
type AdminTab = "overview" | "users" | "league-tiers";
|
type AdminTab = "overview" | "users" | "league-tiers" | "model-performance";
|
||||||
|
|
||||||
// ========================
|
// ========================
|
||||||
// Admin Stat Card
|
// Admin Stat Card
|
||||||
@@ -142,6 +143,7 @@ export default function AdminContent() {
|
|||||||
{ key: "overview", label: t("overview") },
|
{ key: "overview", label: t("overview") },
|
||||||
{ key: "users", label: t("user-management") },
|
{ key: "users", label: t("user-management") },
|
||||||
{ key: "league-tiers", label: "Lig Tier" },
|
{ key: "league-tiers", label: "Lig Tier" },
|
||||||
|
{ key: "model-performance", label: "Model Performansı" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const getUserDisplayName = (user: AdminUserDto) => {
|
const getUserDisplayName = (user: AdminUserDto) => {
|
||||||
@@ -553,6 +555,9 @@ export default function AdminContent() {
|
|||||||
{/* League Tiers Tab */}
|
{/* League Tiers Tab */}
|
||||||
{activeTab === "league-tiers" && <LeagueTiersContent />}
|
{activeTab === "league-tiers" && <LeagueTiersContent />}
|
||||||
|
|
||||||
|
{/* Model Performance Tab */}
|
||||||
|
{activeTab === "model-performance" && <ModelPerformanceContent />}
|
||||||
|
|
||||||
<EditUserModal
|
<EditUserModal
|
||||||
user={editingUser}
|
user={editingUser}
|
||||||
isOpen={!!editingUser}
|
isOpen={!!editingUser}
|
||||||
|
|||||||
@@ -0,0 +1,329 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
SimpleGrid,
|
||||||
|
Card,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Badge,
|
||||||
|
Spinner,
|
||||||
|
Table,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import {
|
||||||
|
NativeSelectRoot,
|
||||||
|
NativeSelectField,
|
||||||
|
} from "@/components/ui/forms/native-select";
|
||||||
|
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||||
|
import { AnimatedCounter } from "@/components/motion";
|
||||||
|
import { useModelPerformance } from "@/lib/api/admin/use-hooks";
|
||||||
|
import type { ModelPerformanceMarketDto } from "@/lib/api/admin/types";
|
||||||
|
import { LuTarget, LuTrendingUp, LuCircleCheck, LuLayers } from "react-icons/lu";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
const MARKET_LABELS: Record<string, string> = {
|
||||||
|
MS: "Maç Sonucu",
|
||||||
|
DC: "Çifte Şans",
|
||||||
|
OU15: "Üst/Alt 1.5",
|
||||||
|
OU25: "Üst/Alt 2.5",
|
||||||
|
OU35: "Üst/Alt 3.5",
|
||||||
|
BTTS: "Karşılıklı Gol",
|
||||||
|
HT: "İlk Yarı Sonucu",
|
||||||
|
HT_OU05: "İY Üst/Alt 0.5",
|
||||||
|
HT_OU15: "İY Üst/Alt 1.5",
|
||||||
|
HTFT: "İlk Yarı / Maç Sonu",
|
||||||
|
OE: "Tek / Çift",
|
||||||
|
CARDS: "Kart Üst/Alt",
|
||||||
|
HCAP: "Handikap",
|
||||||
|
};
|
||||||
|
|
||||||
|
const calibrationLabel = (c: string): { text: string; color: string } => {
|
||||||
|
if (c === "good") return { text: "Dürüst", color: "green" };
|
||||||
|
if (c === "overconfident") return { text: "Fazla iyimser", color: "red" };
|
||||||
|
return { text: "Düşük tahmin", color: "orange" };
|
||||||
|
};
|
||||||
|
|
||||||
|
const roiColor = (roi: number): string =>
|
||||||
|
roi > 3 ? "green" : roi < -3 ? "red" : "gray";
|
||||||
|
|
||||||
|
export default function ModelPerformanceContent() {
|
||||||
|
const [days, setDays] = useState(90);
|
||||||
|
const { data, isLoading } = useModelPerformance(days);
|
||||||
|
const cardBg = useColorModeValue("white", "gray.800");
|
||||||
|
const borderColor = useColorModeValue("gray.200", "gray.700");
|
||||||
|
const headBg = useColorModeValue("gray.50", "whiteAlpha.50");
|
||||||
|
|
||||||
|
const perf = data?.data;
|
||||||
|
const markets = perf?.markets ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack align="stretch" gap={6}>
|
||||||
|
<Flex justify="space-between" align="center" flexWrap="wrap" gap={3}>
|
||||||
|
<Box>
|
||||||
|
<Heading size="md">Model Performansı (İleri Test)</Heading>
|
||||||
|
<Text fontSize="sm" color="fg.muted">
|
||||||
|
Her market için "model %X dedi → gerçekte %Y oldu".
|
||||||
|
Sonuçlanmış gerçek maçlardan otomatik hesaplanır (lookahead yok).
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<NativeSelectRoot maxW="180px">
|
||||||
|
<NativeSelectField
|
||||||
|
value={String(days)}
|
||||||
|
onChange={(e) => setDays(Number(e.target.value))}
|
||||||
|
>
|
||||||
|
<option value="30">Son 30 gün</option>
|
||||||
|
<option value="90">Son 90 gün</option>
|
||||||
|
<option value="180">Son 180 gün</option>
|
||||||
|
<option value="365">Son 1 yıl</option>
|
||||||
|
</NativeSelectField>
|
||||||
|
</NativeSelectRoot>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<Flex justify="center" py={12}>
|
||||||
|
<Spinner size="lg" />
|
||||||
|
</Flex>
|
||||||
|
) : !perf || markets.length === 0 ? (
|
||||||
|
<Card.Root bg={cardBg} borderColor={borderColor}>
|
||||||
|
<Card.Body>
|
||||||
|
<VStack gap={2} py={8}>
|
||||||
|
<Text fontWeight="semibold">Henüz yeterli sonuçlanmış veri yok</Text>
|
||||||
|
<Text fontSize="sm" color="fg.muted" textAlign="center">
|
||||||
|
Tahminler maçlar oynandıkça sonuçlanır. Güvenilir kalibrasyon
|
||||||
|
için market başına ~100 sonuçlanmış tahmin gerekir; bu birkaç
|
||||||
|
hafta içinde birikir.
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</Card.Body>
|
||||||
|
</Card.Root>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Summary cards */}
|
||||||
|
<SimpleGrid columns={{ base: 2, md: 4 }} gap={4}>
|
||||||
|
<StatCard
|
||||||
|
icon={<LuLayers />}
|
||||||
|
label="Sonuçlanan tahmin"
|
||||||
|
value={perf.settled_runs}
|
||||||
|
bg={cardBg}
|
||||||
|
border={borderColor}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
icon={<LuTarget />}
|
||||||
|
label="Sonuçlanan market"
|
||||||
|
value={perf.settled_markets}
|
||||||
|
bg={cardBg}
|
||||||
|
border={borderColor}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
icon={<LuCircleCheck />}
|
||||||
|
label="Dürüst market"
|
||||||
|
value={markets.filter((m) => m.calibration === "good").length}
|
||||||
|
bg={cardBg}
|
||||||
|
border={borderColor}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
icon={<LuTrendingUp />}
|
||||||
|
label="Kârlı market (BET)"
|
||||||
|
value={markets.filter((m) => m.bet_roi_pct > 3).length}
|
||||||
|
bg={cardBg}
|
||||||
|
border={borderColor}
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
{/* Calibration + ROI table */}
|
||||||
|
<Card.Root bg={cardBg} borderColor={borderColor}>
|
||||||
|
<Card.Body p={0}>
|
||||||
|
<Box overflowX="auto">
|
||||||
|
<Table.Root size="sm" striped>
|
||||||
|
<Table.Header bg={headBg}>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.ColumnHeader>Market</Table.ColumnHeader>
|
||||||
|
<Table.ColumnHeader textAlign="end">N</Table.ColumnHeader>
|
||||||
|
<Table.ColumnHeader textAlign="end">
|
||||||
|
Model %
|
||||||
|
</Table.ColumnHeader>
|
||||||
|
<Table.ColumnHeader textAlign="end">
|
||||||
|
Gerçek %
|
||||||
|
</Table.ColumnHeader>
|
||||||
|
<Table.ColumnHeader textAlign="end">
|
||||||
|
Fark
|
||||||
|
</Table.ColumnHeader>
|
||||||
|
<Table.ColumnHeader textAlign="center">
|
||||||
|
Kalibrasyon
|
||||||
|
</Table.ColumnHeader>
|
||||||
|
<Table.ColumnHeader textAlign="end">
|
||||||
|
Bahis
|
||||||
|
</Table.ColumnHeader>
|
||||||
|
<Table.ColumnHeader textAlign="end">
|
||||||
|
İsabet %
|
||||||
|
</Table.ColumnHeader>
|
||||||
|
<Table.ColumnHeader textAlign="end">
|
||||||
|
ROI %
|
||||||
|
</Table.ColumnHeader>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
{markets.map((m) => {
|
||||||
|
const cal = calibrationLabel(m.calibration);
|
||||||
|
return (
|
||||||
|
<Table.Row key={m.market}>
|
||||||
|
<Table.Cell>
|
||||||
|
<VStack align="start" gap={0}>
|
||||||
|
<Text fontWeight="semibold">{m.market}</Text>
|
||||||
|
<Text fontSize="xs" color="fg.muted">
|
||||||
|
{MARKET_LABELS[m.market] ?? m.market}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell textAlign="end">{m.samples}</Table.Cell>
|
||||||
|
<Table.Cell textAlign="end">
|
||||||
|
{m.shown_pct.toFixed(1)}%
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell textAlign="end" fontWeight="semibold">
|
||||||
|
{m.actual_pct.toFixed(1)}%
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell textAlign="end">
|
||||||
|
<Text
|
||||||
|
color={`${cal.color}.500`}
|
||||||
|
fontWeight="medium"
|
||||||
|
>
|
||||||
|
{m.gap > 0 ? "+" : ""}
|
||||||
|
{m.gap.toFixed(1)}
|
||||||
|
</Text>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell textAlign="center">
|
||||||
|
<Badge colorPalette={cal.color} variant="subtle">
|
||||||
|
{cal.text}
|
||||||
|
</Badge>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell textAlign="end">
|
||||||
|
{m.bet_count}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell textAlign="end">
|
||||||
|
{m.bet_count > 0
|
||||||
|
? `${m.bet_hit_pct.toFixed(0)}%`
|
||||||
|
: "—"}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell textAlign="end">
|
||||||
|
{m.bet_count > 0 ? (
|
||||||
|
<Text
|
||||||
|
color={`${roiColor(m.bet_roi_pct)}.500`}
|
||||||
|
fontWeight="semibold"
|
||||||
|
>
|
||||||
|
{m.bet_roi_pct > 0 ? "+" : ""}
|
||||||
|
{m.bet_roi_pct.toFixed(1)}%
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
"—"
|
||||||
|
)}
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</Box>
|
||||||
|
</Card.Body>
|
||||||
|
</Card.Root>
|
||||||
|
|
||||||
|
{/* Decision rationale (why picks come out) */}
|
||||||
|
<Card.Root bg={cardBg} borderColor={borderColor}>
|
||||||
|
<Card.Body>
|
||||||
|
<Heading size="sm" mb={1}>
|
||||||
|
Öneri gerekçesi (karar dağılımı)
|
||||||
|
</Heading>
|
||||||
|
<Text fontSize="sm" color="fg.muted" mb={4}>
|
||||||
|
Her market için betting brain'in ne karar verdiği:
|
||||||
|
BET (oyna), WATCH (izle), REJECT (ele).
|
||||||
|
</Text>
|
||||||
|
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} gap={3}>
|
||||||
|
{markets.map((m) => (
|
||||||
|
<Box
|
||||||
|
key={m.market}
|
||||||
|
p={3}
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
borderRadius="lg"
|
||||||
|
>
|
||||||
|
<HStack justify="space-between" mb={2}>
|
||||||
|
<Text fontWeight="semibold">{m.market}</Text>
|
||||||
|
<Text fontSize="xs" color="fg.muted">
|
||||||
|
{MARKET_LABELS[m.market] ?? ""}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack gap={1} flexWrap="wrap">
|
||||||
|
{Object.entries(m.actions)
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.map(([action, count]) => (
|
||||||
|
<Badge
|
||||||
|
key={action}
|
||||||
|
variant="subtle"
|
||||||
|
colorPalette={
|
||||||
|
action === "BET"
|
||||||
|
? "green"
|
||||||
|
: action === "WATCH"
|
||||||
|
? "blue"
|
||||||
|
: action === "REJECT"
|
||||||
|
? "gray"
|
||||||
|
: "purple"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{action}: {count}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
</Card.Body>
|
||||||
|
</Card.Root>
|
||||||
|
|
||||||
|
<Text fontSize="xs" color="fg.muted">
|
||||||
|
Son güncelleme:{" "}
|
||||||
|
{perf.generated_at
|
||||||
|
? new Date(perf.generated_at).toLocaleString("tr-TR")
|
||||||
|
: "—"}{" "}
|
||||||
|
· Pencere: {perf.window_days} gün · ECE düşük = daha dürüst
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatCard({
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
bg,
|
||||||
|
border,
|
||||||
|
}: {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
bg: string;
|
||||||
|
border: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Card.Root bg={bg} borderColor={border}>
|
||||||
|
<Card.Body>
|
||||||
|
<HStack gap={3}>
|
||||||
|
<Box color="primary.400" fontSize="xl">
|
||||||
|
{icon}
|
||||||
|
</Box>
|
||||||
|
<VStack align="start" gap={0}>
|
||||||
|
<Text fontSize="2xl" fontWeight="bold">
|
||||||
|
<AnimatedCounter value={value} />
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="xs" color="fg.muted">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</Card.Body>
|
||||||
|
</Card.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1239,8 +1239,53 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
marketProbability: uiText("market-probability-short", "Piyasa"),
|
marketProbability: uiText("market-probability-short", "Piyasa"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const leagueConfidence = prediction.match_info?.league_confidence;
|
||||||
|
const leagueConfStyles: Record<string, { color: string; label: string }> = {
|
||||||
|
high: {
|
||||||
|
color: "green",
|
||||||
|
label: uiText("league-conf-high", "Bu ligde model güçlü"),
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
color: "yellow",
|
||||||
|
label: uiText("league-conf-medium", "Bu ligde model orta"),
|
||||||
|
},
|
||||||
|
low: {
|
||||||
|
color: "red",
|
||||||
|
label: uiText("league-conf-low", "Bu ligde model zayıf"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const leagueConfMeta = leagueConfidence
|
||||||
|
? leagueConfStyles[leagueConfidence.label]
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack align="stretch" gap={5}>
|
<VStack align="stretch" gap={5}>
|
||||||
|
{leagueConfidence && leagueConfMeta ? (
|
||||||
|
<HStack
|
||||||
|
justify="space-between"
|
||||||
|
p={2.5}
|
||||||
|
px={3}
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={`${leagueConfMeta.color}.300`}
|
||||||
|
bg={`${leagueConfMeta.color}.50`}
|
||||||
|
borderRadius="lg"
|
||||||
|
_dark={{ bg: `${leagueConfMeta.color}.950` }}
|
||||||
|
flexWrap="wrap"
|
||||||
|
gap={2}
|
||||||
|
>
|
||||||
|
<HStack gap={2}>
|
||||||
|
<Badge colorPalette={leagueConfMeta.color} variant="solid">
|
||||||
|
{leagueConfMeta.label}
|
||||||
|
</Badge>
|
||||||
|
<Text fontSize="xs" color="fg.muted">
|
||||||
|
{uiText("league-conf-basis", "geçmiş performans")}: ROI{" "}
|
||||||
|
{leagueConfidence.bet_roi > 0 ? "+" : ""}
|
||||||
|
{leagueConfidence.bet_roi}% · {leagueConfidence.bet_n}{" "}
|
||||||
|
{uiText("bets-short", "bahis")}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
) : null}
|
||||||
{isLive ? (
|
{isLive ? (
|
||||||
<Box
|
<Box
|
||||||
p={3}
|
p={3}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
AnalyticsOverviewDto,
|
AnalyticsOverviewDto,
|
||||||
LeagueTierDto,
|
LeagueTierDto,
|
||||||
LeagueTierStatsDto,
|
LeagueTierStatsDto,
|
||||||
|
ModelPerformanceDto,
|
||||||
RetrainStatusDto,
|
RetrainStatusDto,
|
||||||
SettingDto,
|
SettingDto,
|
||||||
UpdateLeagueTierDto,
|
UpdateLeagueTierDto,
|
||||||
@@ -30,6 +31,16 @@ const getAnalyticsOverview = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Model Performance (forward-test)
|
||||||
|
const getModelPerformance = (days?: number) => {
|
||||||
|
return apiRequest<ApiResponse<ModelPerformanceDto>>({
|
||||||
|
url: "/admin/model-performance",
|
||||||
|
client: "admin",
|
||||||
|
method: "get",
|
||||||
|
params: days ? { days: String(days) } : undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
const getAllSettings = () => {
|
const getAllSettings = () => {
|
||||||
return apiRequest<ApiResponse<Record<string, string>>>({
|
return apiRequest<ApiResponse<Record<string, string>>>({
|
||||||
@@ -204,6 +215,7 @@ const getRetrainStatus = () => {
|
|||||||
|
|
||||||
export const adminService = {
|
export const adminService = {
|
||||||
getAnalyticsOverview,
|
getAnalyticsOverview,
|
||||||
|
getModelPerformance,
|
||||||
getAllSettings,
|
getAllSettings,
|
||||||
updateSetting,
|
updateSetting,
|
||||||
getAllUsageLimits,
|
getAllUsageLimits,
|
||||||
|
|||||||
@@ -134,3 +134,30 @@ export interface AnalyticsOverviewDto {
|
|||||||
aiHealth?: Record<string, unknown>;
|
aiHealth?: Record<string, unknown>;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================
|
||||||
|
// Model Performance (Forward-Test)
|
||||||
|
// ========================
|
||||||
|
|
||||||
|
export interface ModelPerformanceMarketDto {
|
||||||
|
market: string;
|
||||||
|
samples: number;
|
||||||
|
shown_pct: number;
|
||||||
|
actual_pct: number;
|
||||||
|
gap: number;
|
||||||
|
ece: number;
|
||||||
|
calibration: "good" | "overconfident" | "underconfident";
|
||||||
|
bet_count: number;
|
||||||
|
bet_hit_pct: number;
|
||||||
|
bet_roi_pct: number;
|
||||||
|
actions: Record<string, number>;
|
||||||
|
tiers: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelPerformanceDto {
|
||||||
|
window_days: number;
|
||||||
|
settled_runs: number;
|
||||||
|
settled_markets: number;
|
||||||
|
generated_at: string;
|
||||||
|
markets: ModelPerformanceMarketDto[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import type {
|
|||||||
export const AdminQueryKeys = {
|
export const AdminQueryKeys = {
|
||||||
all: ["admin"] as const,
|
all: ["admin"] as const,
|
||||||
analytics: () => [...AdminQueryKeys.all, "analytics"] as const,
|
analytics: () => [...AdminQueryKeys.all, "analytics"] as const,
|
||||||
|
modelPerformance: (days?: number) =>
|
||||||
|
[...AdminQueryKeys.all, "modelPerformance", days] as const,
|
||||||
settings: () => [...AdminQueryKeys.all, "settings"] as const,
|
settings: () => [...AdminQueryKeys.all, "settings"] as const,
|
||||||
usageLimits: (params?: AdminPaginationParams) =>
|
usageLimits: (params?: AdminPaginationParams) =>
|
||||||
[...AdminQueryKeys.all, "usageLimits", params] as const,
|
[...AdminQueryKeys.all, "usageLimits", params] as const,
|
||||||
@@ -32,6 +34,15 @@ export const useAdminAnalytics = (enabled = true) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Model Performance (forward-test)
|
||||||
|
export const useModelPerformance = (days = 90, enabled = true) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: AdminQueryKeys.modelPerformance(days),
|
||||||
|
queryFn: () => adminService.getModelPerformance(days),
|
||||||
|
enabled,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
export const useAdminSettings = () => {
|
export const useAdminSettings = () => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
|
|||||||
@@ -13,6 +13,13 @@ export interface MatchInfoDto {
|
|||||||
match_date_ms: number;
|
match_date_ms: number;
|
||||||
league_id?: string | null;
|
league_id?: string | null;
|
||||||
is_top_league?: boolean;
|
is_top_league?: boolean;
|
||||||
|
// Backtest-derived per-league confidence (ROI + sample size). null = too little data.
|
||||||
|
league_confidence?: {
|
||||||
|
label: "high" | "medium" | "low";
|
||||||
|
bet_roi: number;
|
||||||
|
bet_n: number;
|
||||||
|
hit: number;
|
||||||
|
} | null;
|
||||||
sport?: SportType;
|
sport?: SportType;
|
||||||
// Live snapshot — set when the match is in play (used to detect stale predictions)
|
// Live snapshot — set when the match is in play (used to detect stale predictions)
|
||||||
status?: string | null;
|
status?: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user