diff --git a/src/components/admin/admin-content.tsx b/src/components/admin/admin-content.tsx index bfe7356..10ed482 100644 --- a/src/components/admin/admin-content.tsx +++ b/src/components/admin/admin-content.tsx @@ -41,8 +41,9 @@ import { useState, useEffect } from "react"; import { useSession } from "next-auth/react"; import { EditUserModal } from "./edit-user-modal"; 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 @@ -142,6 +143,7 @@ export default function AdminContent() { { key: "overview", label: t("overview") }, { key: "users", label: t("user-management") }, { key: "league-tiers", label: "Lig Tier" }, + { key: "model-performance", label: "Model Performansı" }, ]; const getUserDisplayName = (user: AdminUserDto) => { @@ -553,6 +555,9 @@ export default function AdminContent() { {/* League Tiers Tab */} {activeTab === "league-tiers" && } + {/* Model Performance Tab */} + {activeTab === "model-performance" && } + = { + 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 ( + + + + Model Performansı (İleri Test) + + Her market için "model %X dedi → gerçekte %Y oldu". + Sonuçlanmış gerçek maçlardan otomatik hesaplanır (lookahead yok). + + + + setDays(Number(e.target.value))} + > + + + + + + + + + {isLoading ? ( + + + + ) : !perf || markets.length === 0 ? ( + + + + Henüz yeterli sonuçlanmış veri yok + + 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. + + + + + ) : ( + <> + {/* Summary cards */} + + } + label="Sonuçlanan tahmin" + value={perf.settled_runs} + bg={cardBg} + border={borderColor} + /> + } + label="Sonuçlanan market" + value={perf.settled_markets} + bg={cardBg} + border={borderColor} + /> + } + label="Dürüst market" + value={markets.filter((m) => m.calibration === "good").length} + bg={cardBg} + border={borderColor} + /> + } + label="Kârlı market (BET)" + value={markets.filter((m) => m.bet_roi_pct > 3).length} + bg={cardBg} + border={borderColor} + /> + + + {/* Calibration + ROI table */} + + + + + + + Market + N + + Model % + + + Gerçek % + + + Fark + + + Kalibrasyon + + + Bahis + + + İsabet % + + + ROI % + + + + + {markets.map((m) => { + const cal = calibrationLabel(m.calibration); + return ( + + + + {m.market} + + {MARKET_LABELS[m.market] ?? m.market} + + + + {m.samples} + + {m.shown_pct.toFixed(1)}% + + + {m.actual_pct.toFixed(1)}% + + + + {m.gap > 0 ? "+" : ""} + {m.gap.toFixed(1)} + + + + + {cal.text} + + + + {m.bet_count} + + + {m.bet_count > 0 + ? `${m.bet_hit_pct.toFixed(0)}%` + : "—"} + + + {m.bet_count > 0 ? ( + + {m.bet_roi_pct > 0 ? "+" : ""} + {m.bet_roi_pct.toFixed(1)}% + + ) : ( + "—" + )} + + + ); + })} + + + + + + + {/* Decision rationale (why picks come out) */} + + + + Öneri gerekçesi (karar dağılımı) + + + Her market için betting brain'in ne karar verdiği: + BET (oyna), WATCH (izle), REJECT (ele). + + + {markets.map((m) => ( + + + {m.market} + + {MARKET_LABELS[m.market] ?? ""} + + + + {Object.entries(m.actions) + .sort((a, b) => b[1] - a[1]) + .map(([action, count]) => ( + + {action}: {count} + + ))} + + + ))} + + + + + + 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 + + + )} + + ); +} + +function StatCard({ + icon, + label, + value, + bg, + border, +}: { + icon: React.ReactNode; + label: string; + value: number; + bg: string; + border: string; +}) { + return ( + + + + + {icon} + + + + + + + {label} + + + + + + ); +} diff --git a/src/components/matches/prediction-card.tsx b/src/components/matches/prediction-card.tsx index 07b797a..23054ea 100644 --- a/src/components/matches/prediction-card.tsx +++ b/src/components/matches/prediction-card.tsx @@ -1239,8 +1239,53 @@ export default function PredictionCard({ prediction }: PredictionCardProps) { marketProbability: uiText("market-probability-short", "Piyasa"), }; + const leagueConfidence = prediction.match_info?.league_confidence; + const leagueConfStyles: Record = { + 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 ( + {leagueConfidence && leagueConfMeta ? ( + + + + {leagueConfMeta.label} + + + {uiText("league-conf-basis", "geçmiş performans")}: ROI{" "} + {leagueConfidence.bet_roi > 0 ? "+" : ""} + {leagueConfidence.bet_roi}% · {leagueConfidence.bet_n}{" "} + {uiText("bets-short", "bahis")} + + + + ) : null} {isLive ? ( { }); }; +// Model Performance (forward-test) +const getModelPerformance = (days?: number) => { + return apiRequest>({ + url: "/admin/model-performance", + client: "admin", + method: "get", + params: days ? { days: String(days) } : undefined, + }); +}; + // Settings const getAllSettings = () => { return apiRequest>>({ @@ -204,6 +215,7 @@ const getRetrainStatus = () => { export const adminService = { getAnalyticsOverview, + getModelPerformance, getAllSettings, updateSetting, getAllUsageLimits, diff --git a/src/lib/api/admin/types.ts b/src/lib/api/admin/types.ts index aadfee8..c377fa7 100644 --- a/src/lib/api/admin/types.ts +++ b/src/lib/api/admin/types.ts @@ -134,3 +134,30 @@ export interface AnalyticsOverviewDto { aiHealth?: Record; [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; + tiers: Record; +} + +export interface ModelPerformanceDto { + window_days: number; + settled_runs: number; + settled_markets: number; + generated_at: string; + markets: ModelPerformanceMarketDto[]; +} diff --git a/src/lib/api/admin/use-hooks.ts b/src/lib/api/admin/use-hooks.ts index 86085c7..e994e9c 100644 --- a/src/lib/api/admin/use-hooks.ts +++ b/src/lib/api/admin/use-hooks.ts @@ -12,6 +12,8 @@ import type { export const AdminQueryKeys = { all: ["admin"] as const, analytics: () => [...AdminQueryKeys.all, "analytics"] as const, + modelPerformance: (days?: number) => + [...AdminQueryKeys.all, "modelPerformance", days] as const, settings: () => [...AdminQueryKeys.all, "settings"] as const, usageLimits: (params?: AdminPaginationParams) => [...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 export const useAdminSettings = () => { return useQuery({ diff --git a/src/lib/api/predictions/types.ts b/src/lib/api/predictions/types.ts index e9942c5..6726130 100644 --- a/src/lib/api/predictions/types.ts +++ b/src/lib/api/predictions/types.ts @@ -13,6 +13,13 @@ export interface MatchInfoDto { match_date_ms: number; league_id?: string | null; 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; // Live snapshot — set when the match is in play (used to detect stale predictions) status?: string | null;