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;