Files
iddaai-fe/src/components/admin/model-performance-content.tsx
T
fahricansecer 2695cfffb4
Deploy Iddaai Frontend / build-and-deploy (push) Successful in 2m27s
ch
2026-06-02 03:37:07 +03:00

330 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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 &quot;model %X dedi gerçekte %Y oldu&quot;.
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&apos;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>
);
}