v28
This commit is contained in:
@@ -37,8 +37,10 @@ import type {
|
||||
MatchPickDto,
|
||||
MatchPredictionDto,
|
||||
SignalTier,
|
||||
V27EngineDto,
|
||||
} from "@/lib/api/predictions/types";
|
||||
import type { SportType } from "@/lib/api/matches/types";
|
||||
import V28OddsBandPanel from "@/components/matches/v28-odds-band-panel";
|
||||
|
||||
interface PredictionCardProps {
|
||||
prediction: MatchPredictionDto;
|
||||
@@ -1087,6 +1089,10 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
info={uiText("market-board-info", "Modelin her markette gordugu olasilik dagilimi.")}
|
||||
/>
|
||||
|
||||
{prediction.v27_engine ? (
|
||||
<V28OddsBandPanel engine={prediction.v27_engine as V27EngineDto} />
|
||||
) : null}
|
||||
|
||||
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="2xl">
|
||||
<Card.Body gap={4}>
|
||||
<SectionTitle
|
||||
|
||||
@@ -0,0 +1,669 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Card,
|
||||
Grid,
|
||||
HStack,
|
||||
Icon,
|
||||
IconButton,
|
||||
SimpleGrid,
|
||||
Text,
|
||||
VStack,
|
||||
} from "@chakra-ui/react";
|
||||
import {
|
||||
LuBadgeCheck,
|
||||
LuCircleHelp,
|
||||
LuRectangleVertical,
|
||||
LuShieldAlert,
|
||||
LuTarget,
|
||||
LuTrendingUp,
|
||||
} from "react-icons/lu";
|
||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||
import { Tooltip } from "@/components/ui/overlays/tooltip";
|
||||
import type {
|
||||
HtftComboKey,
|
||||
OddsBandCardsDto,
|
||||
OddsBandHtftComboDto,
|
||||
TripleValueEntryDto,
|
||||
V27EngineDto,
|
||||
} from "@/lib/api/predictions/types";
|
||||
|
||||
// ──────────────────────────────────────
|
||||
// Helpers
|
||||
// ──────────────────────────────────────
|
||||
|
||||
function pct(v: number, d = 0): string {
|
||||
if (!v && v !== 0) return "-";
|
||||
return `${(v * 100).toFixed(d)}%`;
|
||||
}
|
||||
|
||||
function edgeStr(edge: number): string {
|
||||
const sign = edge > 0 ? "+" : "";
|
||||
return `${sign}${(edge * 100).toFixed(1)}%`;
|
||||
}
|
||||
|
||||
const TRIPLE_VALUE_LABELS: Record<string, string> = {
|
||||
home: "MS Ev",
|
||||
away: "MS Dep",
|
||||
ou25_over: "ÜST 2.5",
|
||||
btts_yes: "KG Var",
|
||||
ou15_over: "ÜST 1.5",
|
||||
ou35_over: "ÜST 3.5",
|
||||
dc_1x: "ÇŞ 1X",
|
||||
dc_x2: "ÇŞ X2",
|
||||
dc_12: "ÇŞ 12",
|
||||
ht_home: "İY Ev",
|
||||
ht_away: "İY Dep",
|
||||
ht_ou05_over: "İY ÜST 0.5",
|
||||
ht_ou15_over: "İY ÜST 1.5",
|
||||
oe_odd: "Tek",
|
||||
cards_over: "Kart ÜST",
|
||||
htft_11: "İY/MS 1/1",
|
||||
htft_1x: "İY/MS 1/X",
|
||||
htft_12: "İY/MS 1/2",
|
||||
htft_x1: "İY/MS X/1",
|
||||
htft_xx: "İY/MS X/X",
|
||||
htft_x2: "İY/MS X/2",
|
||||
htft_21: "İY/MS 2/1",
|
||||
htft_2x: "İY/MS 2/X",
|
||||
htft_22: "İY/MS 2/2",
|
||||
};
|
||||
|
||||
const HTFT_DISPLAY: Record<HtftComboKey, string> = {
|
||||
"11": "1/1",
|
||||
"1x": "1/X",
|
||||
"12": "1/2",
|
||||
x1: "X/1",
|
||||
xx: "X/X",
|
||||
x2: "X/2",
|
||||
"21": "2/1",
|
||||
"2x": "2/X",
|
||||
"22": "2/2",
|
||||
};
|
||||
|
||||
const HTFT_ROWS: HtftComboKey[][] = [
|
||||
["11", "1x", "12"],
|
||||
["x1", "xx", "x2"],
|
||||
["21", "2x", "22"],
|
||||
];
|
||||
|
||||
function TooltipIcon({ content }: { content: string }) {
|
||||
return (
|
||||
<Tooltip
|
||||
content={content}
|
||||
showArrow
|
||||
positioning={{ placement: "top" }}
|
||||
contentProps={{ maxW: "260px", fontSize: "xs", px: 3, py: 2 }}
|
||||
>
|
||||
<IconButton
|
||||
aria-label="Bilgi"
|
||||
variant="ghost"
|
||||
size="2xs"
|
||||
colorPalette="gray"
|
||||
>
|
||||
<LuCircleHelp />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionTitle({
|
||||
icon,
|
||||
title,
|
||||
info,
|
||||
}: {
|
||||
icon: React.ElementType;
|
||||
title: string;
|
||||
info?: string;
|
||||
}) {
|
||||
return (
|
||||
<HStack justify="space-between" w="full">
|
||||
<HStack gap={2}>
|
||||
<Icon as={icon} boxSize={4.5} color="fg.muted" />
|
||||
<Text fontSize="lg" fontWeight="semibold">
|
||||
{title}
|
||||
</Text>
|
||||
</HStack>
|
||||
{info ? <TooltipIcon content={info} /> : null}
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────
|
||||
// Triple Value Card
|
||||
// ──────────────────────────────────────
|
||||
|
||||
function TripleValueCard({
|
||||
label,
|
||||
entry,
|
||||
}: {
|
||||
label: string;
|
||||
entry: TripleValueEntryDto;
|
||||
}) {
|
||||
const isValue = entry.is_value;
|
||||
const hasSample = entry.band_sample >= 5;
|
||||
|
||||
const cardBg = useColorModeValue(
|
||||
isValue ? "green.50" : "gray.50",
|
||||
isValue ? "green.950" : "whiteAlpha.50",
|
||||
);
|
||||
const borderCol = useColorModeValue(
|
||||
isValue ? "green.300" : "gray.200",
|
||||
isValue ? "green.700" : "gray.700",
|
||||
);
|
||||
const edgeColor = entry.edge > 0.03 ? "green.500" : entry.edge < -0.03 ? "red.400" : "fg.muted";
|
||||
|
||||
return (
|
||||
<Box
|
||||
p={3}
|
||||
bg={cardBg}
|
||||
borderWidth="1px"
|
||||
borderColor={borderCol}
|
||||
borderRadius="xl"
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
>
|
||||
{isValue && (
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
h="3px"
|
||||
bgGradient="to-r"
|
||||
gradientFrom="green.400"
|
||||
gradientTo="teal.400"
|
||||
/>
|
||||
)}
|
||||
<VStack align="stretch" gap={1.5}>
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="xs" fontWeight="semibold" color="fg.muted">
|
||||
{label}
|
||||
</Text>
|
||||
{isValue ? (
|
||||
<Badge
|
||||
colorPalette="green"
|
||||
variant="solid"
|
||||
fontSize="2xs"
|
||||
borderRadius="full"
|
||||
>
|
||||
DEĞER
|
||||
</Badge>
|
||||
) : hasSample ? (
|
||||
<Badge variant="outline" fontSize="2xs" borderRadius="full">
|
||||
PAS
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge
|
||||
colorPalette="gray"
|
||||
variant="subtle"
|
||||
fontSize="2xs"
|
||||
borderRadius="full"
|
||||
>
|
||||
YETERSİZ
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<Text fontSize="xl" fontWeight="bold" color={edgeColor}>
|
||||
{edgeStr(entry.edge)}
|
||||
</Text>
|
||||
|
||||
<HStack gap={2} flexWrap="wrap">
|
||||
<Text fontSize="2xs" color="fg.muted">
|
||||
Band: {pct(entry.band_rate, 1)}
|
||||
</Text>
|
||||
<Text fontSize="2xs" color="fg.muted">
|
||||
Oran: {pct(entry.implied_prob, 1)}
|
||||
</Text>
|
||||
{entry.confirmations !== undefined && (
|
||||
<Text fontSize="2xs" color="fg.muted">
|
||||
Onay: {entry.confirmations}/2
|
||||
</Text>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<Text fontSize="2xs" color="fg.muted">
|
||||
{entry.band_sample} maç
|
||||
</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────
|
||||
// Cards Section
|
||||
// ──────────────────────────────────────
|
||||
|
||||
function ProgressBar({
|
||||
value,
|
||||
max,
|
||||
color,
|
||||
}: {
|
||||
value: number;
|
||||
max: number;
|
||||
color: string;
|
||||
}) {
|
||||
const trackBg = useColorModeValue("gray.100", "gray.700");
|
||||
const w = max > 0 ? Math.min(100, (value / max) * 100) : 0;
|
||||
return (
|
||||
<Box
|
||||
h="10px"
|
||||
w="full"
|
||||
bg={trackBg}
|
||||
borderRadius="full"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Box
|
||||
h="full"
|
||||
w={`${w}%`}
|
||||
bg={color}
|
||||
borderRadius="full"
|
||||
transition="width 0.4s ease"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function CardsSection({ cards }: { cards: OddsBandCardsDto }) {
|
||||
const cardBg = useColorModeValue("white", "gray.800");
|
||||
const borderColor = useColorModeValue("gray.200", "gray.700");
|
||||
const hasData = cards.sample >= 3;
|
||||
|
||||
if (!hasData) {
|
||||
return (
|
||||
<Box
|
||||
p={4}
|
||||
bg={cardBg}
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
borderRadius="xl"
|
||||
>
|
||||
<HStack gap={2} mb={2}>
|
||||
<Icon as={LuRectangleVertical} boxSize={4} color="yellow.500" />
|
||||
<Text fontSize="sm" fontWeight="semibold">
|
||||
Kart Analizi
|
||||
</Text>
|
||||
</HStack>
|
||||
<Text fontSize="sm" color="fg.muted">
|
||||
Yetersiz veri — henüz yeterli maç örneği bulunamadı.
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const overPct = cards.combined_over_rate * 100;
|
||||
const overColor =
|
||||
overPct >= 65 ? "red.400" : overPct >= 50 ? "orange.400" : "green.400";
|
||||
|
||||
return (
|
||||
<Box
|
||||
p={4}
|
||||
bg={cardBg}
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
borderRadius="xl"
|
||||
>
|
||||
<HStack justify="space-between" mb={4}>
|
||||
<HStack gap={2}>
|
||||
<Icon as={LuRectangleVertical} boxSize={4} color="yellow.500" />
|
||||
<Text fontSize="sm" fontWeight="semibold">
|
||||
Kart Analizi
|
||||
</Text>
|
||||
</HStack>
|
||||
<Badge variant="outline" fontSize="2xs" borderRadius="full">
|
||||
{cards.sample} maç
|
||||
</Badge>
|
||||
</HStack>
|
||||
|
||||
<VStack align="stretch" gap={3}>
|
||||
{/* Referee profile */}
|
||||
<Box>
|
||||
<HStack justify="space-between" mb={1}>
|
||||
<Text fontSize="xs" color="fg.muted">
|
||||
Hakem Profili
|
||||
</Text>
|
||||
<Text fontSize="xs" fontWeight="semibold">
|
||||
Ort: {cards.referee_avg.toFixed(1)} kart
|
||||
</Text>
|
||||
</HStack>
|
||||
<ProgressBar
|
||||
value={cards.referee_over_rate * 100}
|
||||
max={100}
|
||||
color="purple.400"
|
||||
/>
|
||||
<HStack justify="space-between" mt={0.5}>
|
||||
<Text fontSize="2xs" color="fg.muted">
|
||||
Üst oranı: {pct(cards.referee_over_rate, 0)}
|
||||
</Text>
|
||||
<Text fontSize="2xs" color="fg.muted">
|
||||
{cards.referee_sample} maç
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
|
||||
{/* Team profile */}
|
||||
<Box>
|
||||
<HStack justify="space-between" mb={1}>
|
||||
<Text fontSize="xs" color="fg.muted">
|
||||
Takım Profili
|
||||
</Text>
|
||||
<Text fontSize="xs" fontWeight="semibold">
|
||||
Ort: {cards.team_avg.toFixed(1)} kart
|
||||
</Text>
|
||||
</HStack>
|
||||
<ProgressBar
|
||||
value={cards.team_over_rate * 100}
|
||||
max={100}
|
||||
color="blue.400"
|
||||
/>
|
||||
<HStack justify="space-between" mt={0.5}>
|
||||
<Text fontSize="2xs" color="fg.muted">
|
||||
Üst oranı: {pct(cards.team_over_rate, 0)}
|
||||
</Text>
|
||||
<Text fontSize="2xs" color="fg.muted">
|
||||
{cards.team_sample} maç
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
|
||||
{/* Combined */}
|
||||
<Box
|
||||
p={3}
|
||||
bg={useColorModeValue("gray.50", "whiteAlpha.50")}
|
||||
borderRadius="lg"
|
||||
>
|
||||
<HStack justify="space-between">
|
||||
<VStack align="start" gap={0}>
|
||||
<Text fontSize="xs" fontWeight="semibold">
|
||||
Kombine ÜST Oranı
|
||||
</Text>
|
||||
<Text fontSize="2xs" color="fg.muted">
|
||||
%60 Hakem + %40 Takım ağırlıklı
|
||||
</Text>
|
||||
</VStack>
|
||||
<Text fontSize="2xl" fontWeight="bold" color={overColor}>
|
||||
{overPct.toFixed(0)}%
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
</VStack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────
|
||||
// HTFT 3x3 Grid
|
||||
// ──────────────────────────────────────
|
||||
|
||||
function HtftGrid({
|
||||
htft,
|
||||
}: {
|
||||
htft: Record<HtftComboKey, OddsBandHtftComboDto>;
|
||||
}) {
|
||||
const cardBg = useColorModeValue("white", "gray.800");
|
||||
const borderColor = useColorModeValue("gray.200", "gray.700");
|
||||
|
||||
// Find the max rate for highlighting
|
||||
let maxRate = 0;
|
||||
let maxKey: HtftComboKey = "11";
|
||||
let totalSample = 0;
|
||||
for (const [key, val] of Object.entries(htft) as [
|
||||
HtftComboKey,
|
||||
OddsBandHtftComboDto,
|
||||
][]) {
|
||||
if (val.rate > maxRate) {
|
||||
maxRate = val.rate;
|
||||
maxKey = key;
|
||||
}
|
||||
totalSample = Math.max(totalSample, val.sample);
|
||||
}
|
||||
|
||||
const getCellColor = (rate: number, isMax: boolean) => {
|
||||
if (isMax) return { bg: "green.500", text: "white" };
|
||||
if (rate >= 0.2) return { bg: "green.100", text: "green.800" };
|
||||
if (rate >= 0.12) return { bg: "yellow.100", text: "yellow.800" };
|
||||
if (rate >= 0.06) return { bg: "orange.50", text: "orange.700" };
|
||||
return { bg: "gray.50", text: "gray.500" };
|
||||
};
|
||||
|
||||
const getCellColorDark = (rate: number, isMax: boolean) => {
|
||||
if (isMax) return { bg: "green.600", text: "white" };
|
||||
if (rate >= 0.2) return { bg: "green.900", text: "green.200" };
|
||||
if (rate >= 0.12) return { bg: "yellow.900", text: "yellow.200" };
|
||||
if (rate >= 0.06) return { bg: "orange.900", text: "orange.200" };
|
||||
return { bg: "whiteAlpha.50", text: "gray.500" };
|
||||
};
|
||||
|
||||
const lightMode = useColorModeValue(true, false);
|
||||
|
||||
return (
|
||||
<Box
|
||||
p={4}
|
||||
bg={cardBg}
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
borderRadius="xl"
|
||||
>
|
||||
<HStack justify="space-between" mb={4}>
|
||||
<HStack gap={2}>
|
||||
<Icon as={LuTarget} boxSize={4} color="teal.500" />
|
||||
<Text fontSize="sm" fontWeight="semibold">
|
||||
İY/MS Kombinasyonları
|
||||
</Text>
|
||||
</HStack>
|
||||
<TooltipIcon content="İlk yarı sonucu ve maç sonucu kombinasyonlarının tarihsel oran bandındaki gerçekleşme oranları." />
|
||||
</HStack>
|
||||
|
||||
{/* Column headers */}
|
||||
<Grid templateColumns="50px repeat(3, 1fr)" gap={1.5} mb={1.5}>
|
||||
<Box />
|
||||
<Text fontSize="2xs" fontWeight="bold" textAlign="center" color="fg.muted">
|
||||
MS 1
|
||||
</Text>
|
||||
<Text fontSize="2xs" fontWeight="bold" textAlign="center" color="fg.muted">
|
||||
MS X
|
||||
</Text>
|
||||
<Text fontSize="2xs" fontWeight="bold" textAlign="center" color="fg.muted">
|
||||
MS 2
|
||||
</Text>
|
||||
</Grid>
|
||||
|
||||
{/* Grid rows */}
|
||||
{HTFT_ROWS.map((row, rowIdx) => {
|
||||
const rowLabels = ["İY 1", "İY X", "İY 2"];
|
||||
return (
|
||||
<Grid
|
||||
key={rowIdx}
|
||||
templateColumns="50px repeat(3, 1fr)"
|
||||
gap={1.5}
|
||||
mb={1.5}
|
||||
>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Text fontSize="2xs" fontWeight="bold" color="fg.muted">
|
||||
{rowLabels[rowIdx]}
|
||||
</Text>
|
||||
</Box>
|
||||
{row.map((comboKey) => {
|
||||
const data = htft[comboKey] || { rate: 0, sample: 0 };
|
||||
const isMax = comboKey === maxKey && maxRate > 0.05;
|
||||
const colors = lightMode
|
||||
? getCellColor(data.rate, isMax)
|
||||
: getCellColorDark(data.rate, isMax);
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={comboKey}
|
||||
py={2.5}
|
||||
px={2}
|
||||
bg={colors.bg}
|
||||
borderRadius="lg"
|
||||
textAlign="center"
|
||||
position="relative"
|
||||
transition="all 0.2s"
|
||||
_hover={{ transform: "scale(1.04)" }}
|
||||
>
|
||||
<Text
|
||||
fontSize="xs"
|
||||
fontWeight="bold"
|
||||
color={colors.text}
|
||||
mb={0.5}
|
||||
>
|
||||
{HTFT_DISPLAY[comboKey]}
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="lg"
|
||||
fontWeight="extrabold"
|
||||
color={colors.text}
|
||||
>
|
||||
{pct(data.rate, 0)}
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="2xs"
|
||||
color={isMax ? "whiteAlpha.800" : "fg.muted"}
|
||||
>
|
||||
{data.sample} maç
|
||||
</Text>
|
||||
{isMax && (
|
||||
<Icon
|
||||
as={LuBadgeCheck}
|
||||
position="absolute"
|
||||
top={1}
|
||||
right={1}
|
||||
boxSize={3.5}
|
||||
color="white"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Best combo callout */}
|
||||
{maxRate > 0.05 && (
|
||||
<Box
|
||||
mt={2}
|
||||
p={2.5}
|
||||
bg={useColorModeValue("green.50", "green.950")}
|
||||
borderRadius="lg"
|
||||
>
|
||||
<HStack gap={2}>
|
||||
<Icon as={LuTrendingUp} boxSize={4} color="green.500" />
|
||||
<Text fontSize="xs" fontWeight="semibold">
|
||||
En güçlü:{" "}
|
||||
<Text as="span" color="green.500">
|
||||
{HTFT_DISPLAY[maxKey]} ({pct(maxRate, 0)})
|
||||
</Text>
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────
|
||||
// Main Panel Export
|
||||
// ──────────────────────────────────────
|
||||
|
||||
interface V28OddsBandPanelProps {
|
||||
engine: V27EngineDto;
|
||||
}
|
||||
|
||||
export default function V28OddsBandPanel({ engine }: V28OddsBandPanelProps) {
|
||||
const cardBg = useColorModeValue("white", "gray.800");
|
||||
const borderColor = useColorModeValue("gray.200", "gray.700");
|
||||
|
||||
const tripleValue = engine.triple_value;
|
||||
const cards = engine.odds_band?.cards as OddsBandCardsDto | undefined;
|
||||
const htft = engine.odds_band?.htft as
|
||||
| Record<HtftComboKey, OddsBandHtftComboDto>
|
||||
| undefined;
|
||||
|
||||
// Filter out HTFT triple-value entries from the main grid (shown in HTFT section)
|
||||
const mainValueEntries = Object.entries(tripleValue || {}).filter(
|
||||
([key]) => !key.startsWith("htft_"),
|
||||
);
|
||||
|
||||
// Separate value hits from non-hits for priority ordering
|
||||
const valueHits = mainValueEntries.filter(([, e]) => e.is_value);
|
||||
const valueNon = mainValueEntries.filter(([, e]) => !e.is_value);
|
||||
const orderedEntries = [...valueHits, ...valueNon];
|
||||
|
||||
const hasTriple = orderedEntries.length > 0;
|
||||
const hasCards = cards && cards.sample >= 1;
|
||||
const hasHtft = htft && Object.values(htft).some((v) => v.sample > 0);
|
||||
|
||||
if (!hasTriple && !hasCards && !hasHtft) return null;
|
||||
|
||||
return (
|
||||
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="2xl">
|
||||
<Card.Body gap={5}>
|
||||
<SectionTitle
|
||||
icon={LuShieldAlert}
|
||||
title="V28 Oran Bandı Analizi"
|
||||
info="Geçmiş maçlarda benzer oranlarda gerçekleşen sonuçların istatistiksel analizi. Triple Value, Kart Profili ve İY/MS kombinasyonlarını içerir."
|
||||
/>
|
||||
|
||||
{/* Engine version badge */}
|
||||
<HStack>
|
||||
<Badge colorPalette="purple" variant="subtle" borderRadius="full" fontSize="2xs">
|
||||
{engine.version}
|
||||
</Badge>
|
||||
{engine.consensus && (
|
||||
<Badge
|
||||
colorPalette={engine.consensus === "AGREE" ? "green" : "orange"}
|
||||
variant="solid"
|
||||
borderRadius="full"
|
||||
fontSize="2xs"
|
||||
>
|
||||
{engine.consensus === "AGREE" ? "Motorlar Uyumlu" : "Motorlar Farklı"}
|
||||
</Badge>
|
||||
)}
|
||||
{valueHits.length > 0 && (
|
||||
<Badge colorPalette="green" variant="outline" borderRadius="full" fontSize="2xs">
|
||||
{valueHits.length} Değer Sinyali
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
{/* Triple Value Grid */}
|
||||
{hasTriple && (
|
||||
<Box>
|
||||
<HStack mb={3} gap={2}>
|
||||
<Icon as={LuTarget} boxSize={4} color="blue.500" />
|
||||
<Text fontSize="sm" fontWeight="semibold">
|
||||
Değer Tespiti (Triple Value)
|
||||
</Text>
|
||||
<TooltipIcon content="Model olasılığı, oran bandı istatistiği ve piyasa oranı karşılaştırması. Edge pozitifse model avantaj görüyor demektir." />
|
||||
</HStack>
|
||||
<SimpleGrid columns={{ base: 2, md: 3, xl: 4 }} gap={2.5}>
|
||||
{orderedEntries.map(([key, entry]) => (
|
||||
<TripleValueCard
|
||||
key={key}
|
||||
label={TRIPLE_VALUE_LABELS[key] || key}
|
||||
entry={entry}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Cards + HTFT side by side on large screens */}
|
||||
{(hasCards || hasHtft) && (
|
||||
<Grid
|
||||
templateColumns={{ base: "1fr", xl: hasCards && hasHtft ? "1fr 1fr" : "1fr" }}
|
||||
gap={4}
|
||||
>
|
||||
{hasCards && <CardsSection cards={cards} />}
|
||||
{hasHtft && <HtftGrid htft={htft} />}
|
||||
</Grid>
|
||||
)}
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user