gg
This commit is contained in:
+30
-1
@@ -342,7 +342,36 @@
|
|||||||
"risk-label": "Risk",
|
"risk-label": "Risk",
|
||||||
"data-quality-label": "Data Quality",
|
"data-quality-label": "Data Quality",
|
||||||
"rejected-matches-title": "Rejected Matches",
|
"rejected-matches-title": "Rejected Matches",
|
||||||
"no-suggestion-yet": "No coupon has been generated yet. Choose a strategy and click AI Suggest."
|
"no-suggestion-yet": "No coupon has been generated yet. Choose a strategy and click AI Suggest.",
|
||||||
|
"freq-engine-title": "Frequency Engine",
|
||||||
|
"freq-engine-subtitle": "Analyzes teams' historical performance by odds band. Uses statistical database scans instead of AI models.",
|
||||||
|
"freq-suggest": "Generate Frequency Coupon",
|
||||||
|
"freq-suggest-loading": "Running frequency analysis...",
|
||||||
|
"freq-min-signal": "Minimum Signal",
|
||||||
|
"freq-min-signal-help": "Combined signal threshold (0.50-0.99). Lower = more matches, higher = more precise results.",
|
||||||
|
"freq-markets": "Markets",
|
||||||
|
"freq-markets-help": "Select markets to analyze. Leave empty to scan all markets.",
|
||||||
|
"freq-ev-label": "Expected Value (EV)",
|
||||||
|
"freq-ev-help": "Hit Rate × Total Odds. Above 1.0 means +EV (profitable).",
|
||||||
|
"freq-hit-rate": "Est. Hit Rate",
|
||||||
|
"freq-hit-rate-help": "Combined historical hit rate of all bets in the coupon.",
|
||||||
|
"freq-ev-positive": "+EV Positive",
|
||||||
|
"freq-ev-negative": "EV Negative",
|
||||||
|
"freq-home-signal": "Home Signal",
|
||||||
|
"freq-away-signal": "Away Signal",
|
||||||
|
"freq-combined-signal": "Combined Signal",
|
||||||
|
"freq-odds-band": "Odds Band",
|
||||||
|
"freq-league-profile": "League Profile",
|
||||||
|
"freq-league-golcu": "High-Scoring",
|
||||||
|
"freq-league-defansif": "Defensive",
|
||||||
|
"freq-league-normal": "Normal",
|
||||||
|
"freq-match-count": "Past Matches",
|
||||||
|
"freq-reasoning-title": "Analysis Details",
|
||||||
|
"freq-no-result": "No matches found meeting frequency analysis criteria. Try lowering the signal threshold.",
|
||||||
|
"freq-mode-active": "Frequency Engine active",
|
||||||
|
"ai-mode-active": "AI Engine active",
|
||||||
|
"engine-mode-label": "Engine Mode",
|
||||||
|
"engine-mode-help": "AI: Gemini-based AI prediction. Frequency: Database-driven statistical analysis."
|
||||||
},
|
},
|
||||||
|
|
||||||
"profile": {
|
"profile": {
|
||||||
|
|||||||
+30
-1
@@ -331,7 +331,36 @@
|
|||||||
"risk-label": "Risk",
|
"risk-label": "Risk",
|
||||||
"data-quality-label": "Veri Kalitesi",
|
"data-quality-label": "Veri Kalitesi",
|
||||||
"rejected-matches-title": "Elenen Maçlar",
|
"rejected-matches-title": "Elenen Maçlar",
|
||||||
"no-suggestion-yet": "Henüz kupon üretilmedi. Strateji seçip AI Öner butonuna basın."
|
"no-suggestion-yet": "Henüz kupon üretilmedi. Strateji seçip AI Öner butonuna basın.",
|
||||||
|
"freq-engine-title": "Frekans Motoru",
|
||||||
|
"freq-engine-subtitle": "Takımların oran bandına göre tarihsel performansını analiz eder. AI modeli yerine istatistiksel veritabanı taraması kullanır.",
|
||||||
|
"freq-suggest": "Frekans Kuponu Oluştur",
|
||||||
|
"freq-suggest-loading": "Frekans analizi çalışıyor...",
|
||||||
|
"freq-min-signal": "Minimum Sinyal",
|
||||||
|
"freq-min-signal-help": "Kombinasyon sinyal eşiği (0.50-0.99). Düşürürseniz daha fazla maç bulunur, yükseltirseniz daha kesin sonuçlar gelir.",
|
||||||
|
"freq-markets": "Marketler",
|
||||||
|
"freq-markets-help": "Analiz edilecek marketleri seçin. Boş bırakırsanız tüm marketler taranır.",
|
||||||
|
"freq-ev-label": "Beklenen Değer (EV)",
|
||||||
|
"freq-ev-help": "Hit Rate × Toplam Oran. 1.0'ın üzeri +EV (karlı) anlamına gelir.",
|
||||||
|
"freq-hit-rate": "Tahmini İsabet",
|
||||||
|
"freq-hit-rate-help": "Tüm bahislerin birleşik tarihsel isabet oranı.",
|
||||||
|
"freq-ev-positive": "+EV Pozitif",
|
||||||
|
"freq-ev-negative": "EV Negatif",
|
||||||
|
"freq-home-signal": "Ev Sinyali",
|
||||||
|
"freq-away-signal": "Dep Sinyali",
|
||||||
|
"freq-combined-signal": "Kombine Sinyal",
|
||||||
|
"freq-odds-band": "Oran Bandı",
|
||||||
|
"freq-league-profile": "Lig Profili",
|
||||||
|
"freq-league-golcu": "Golcü",
|
||||||
|
"freq-league-defansif": "Defansif",
|
||||||
|
"freq-league-normal": "Normal",
|
||||||
|
"freq-match-count": "Geçmiş Maç",
|
||||||
|
"freq-reasoning-title": "Analiz Detayları",
|
||||||
|
"freq-no-result": "Frekans analizine uygun yeterli maç bulunamadı. Sinyal eşiğini düşürmeyi deneyin.",
|
||||||
|
"freq-mode-active": "Frekans Motoru aktif",
|
||||||
|
"ai-mode-active": "AI Motoru aktif",
|
||||||
|
"engine-mode-label": "Motor Seçimi",
|
||||||
|
"engine-mode-help": "AI: Gemini tabanlı yapay zeka tahmini. Frekans: Veritabanı tabanlı istatistiksel analiz."
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"title": "Profil",
|
"title": "Profil",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
LuBadgeAlert,
|
LuBadgeAlert,
|
||||||
LuCheck,
|
LuCheck,
|
||||||
LuCircleHelp,
|
LuCircleHelp,
|
||||||
|
LuDatabase,
|
||||||
LuEye,
|
LuEye,
|
||||||
LuEyeOff,
|
LuEyeOff,
|
||||||
LuLayers3,
|
LuLayers3,
|
||||||
@@ -38,6 +39,7 @@ import {
|
|||||||
import { SlideUp } from "@/components/motion";
|
import { SlideUp } from "@/components/motion";
|
||||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||||
import { Tooltip } from "@/components/ui/overlays/tooltip";
|
import { Tooltip } from "@/components/ui/overlays/tooltip";
|
||||||
|
import FrequencyPanel from "@/components/coupons/frequency-panel";
|
||||||
import { useSuggestCoupon } from "@/lib/api/coupons/use-hooks";
|
import { useSuggestCoupon } from "@/lib/api/coupons/use-hooks";
|
||||||
import type {
|
import type {
|
||||||
CouponItemDto,
|
CouponItemDto,
|
||||||
@@ -352,6 +354,7 @@ export default function CouponBuilderContent() {
|
|||||||
SmartCouponResultDto | undefined
|
SmartCouponResultDto | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
const [matchCount, setMatchCount] = React.useState<number>(5); // Default: 5 matches
|
const [matchCount, setMatchCount] = React.useState<number>(5); // Default: 5 matches
|
||||||
|
const [engineMode, setEngineMode] = React.useState<"ai" | "frequency">("ai");
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!upcomingQuery.data && !upcomingQuery.isPending) {
|
if (!upcomingQuery.data && !upcomingQuery.isPending) {
|
||||||
@@ -763,6 +766,42 @@ export default function CouponBuilderContent() {
|
|||||||
</HStack>
|
</HStack>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Body pt={0}>
|
<Card.Body pt={0}>
|
||||||
|
{/* Engine Mode Toggle */}
|
||||||
|
<VStack align="stretch" gap={2} mb={4}>
|
||||||
|
<HStack gap={2}>
|
||||||
|
<Icon as={engineMode === "ai" ? LuSparkles : LuDatabase} color={engineMode === "ai" ? "teal.500" : "cyan.500"} />
|
||||||
|
<Text fontWeight="semibold" fontSize="sm">{t("engine-mode-label")}</Text>
|
||||||
|
<InfoIcon content={t("engine-mode-help")} label={t("engine-mode-label")} />
|
||||||
|
</HStack>
|
||||||
|
<HStack gap={2}>
|
||||||
|
<Badge
|
||||||
|
colorPalette={engineMode === "ai" ? "teal" : "gray"}
|
||||||
|
variant={engineMode === "ai" ? "solid" : "outline"}
|
||||||
|
cursor="pointer" px={3} py={1}
|
||||||
|
onClick={() => setEngineMode("ai")}
|
||||||
|
>
|
||||||
|
<LuSparkles /> AI
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
colorPalette={engineMode === "frequency" ? "cyan" : "gray"}
|
||||||
|
variant={engineMode === "frequency" ? "solid" : "outline"}
|
||||||
|
cursor="pointer" px={3} py={1}
|
||||||
|
onClick={() => setEngineMode("frequency")}
|
||||||
|
>
|
||||||
|
<LuDatabase /> Frekans
|
||||||
|
</Badge>
|
||||||
|
</HStack>
|
||||||
|
<Text fontSize="xs" color={engineMode === "ai" ? "teal.500" : "cyan.500"}>
|
||||||
|
{engineMode === "ai" ? t("ai-mode-active") : t("freq-mode-active")}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
<Separator mb={4} />
|
||||||
|
|
||||||
|
{engineMode === "frequency" ? (
|
||||||
|
<FrequencyPanel />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<Text
|
<Text
|
||||||
fontSize="xs"
|
fontSize="xs"
|
||||||
color="fg.muted"
|
color="fg.muted"
|
||||||
@@ -920,6 +959,8 @@ export default function CouponBuilderContent() {
|
|||||||
? t("manual-selection-helper")
|
? t("manual-selection-helper")
|
||||||
: t("automatic-selection-helper")}
|
: t("automatic-selection-helper")}
|
||||||
</Text>
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,451 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Flex,
|
||||||
|
Grid,
|
||||||
|
Heading,
|
||||||
|
HStack,
|
||||||
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
Separator,
|
||||||
|
Text,
|
||||||
|
VStack,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
LuBadgeAlert,
|
||||||
|
LuBarChart3,
|
||||||
|
LuCircleHelp,
|
||||||
|
LuDatabase,
|
||||||
|
LuTrendingUp,
|
||||||
|
LuZap,
|
||||||
|
} from "react-icons/lu";
|
||||||
|
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||||
|
import { Tooltip } from "@/components/ui/overlays/tooltip";
|
||||||
|
import { useGenerateFrequencyCoupon } from "@/lib/api/coupons/use-hooks";
|
||||||
|
import type {
|
||||||
|
FrequencyCouponResultDto,
|
||||||
|
FrequencyCouponBetDto,
|
||||||
|
} from "@/lib/api/coupons/types";
|
||||||
|
import { ApiError } from "@/lib/api/create-api-client";
|
||||||
|
import { useCouponStore } from "@/lib/stores/coupon-store";
|
||||||
|
|
||||||
|
const AVAILABLE_MARKETS = ["OU1.5", "OU2.5", "OU3.5", "BTTS", "MS"];
|
||||||
|
|
||||||
|
function InfoIcon({ content, label }: { content: string; label: string }) {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
content={content}
|
||||||
|
showArrow
|
||||||
|
positioning={{ placement: "top" }}
|
||||||
|
contentProps={{ maxW: "260px", fontSize: "xs", px: 3, py: 2 }}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
aria-label={label}
|
||||||
|
variant="ghost"
|
||||||
|
size="2xs"
|
||||||
|
colorPalette="gray"
|
||||||
|
>
|
||||||
|
<LuCircleHelp />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const profileColor = (p: string) =>
|
||||||
|
({ GOLCU: "red", DEFANSIF: "blue", NORMAL: "gray" })[p] || "gray";
|
||||||
|
|
||||||
|
const profileLabel = (p: string, t: ReturnType<typeof useTranslations>) =>
|
||||||
|
({
|
||||||
|
GOLCU: t("freq-league-golcu"),
|
||||||
|
DEFANSIF: t("freq-league-defansif"),
|
||||||
|
NORMAL: t("freq-league-normal"),
|
||||||
|
})[p] || p;
|
||||||
|
|
||||||
|
export default function FrequencyPanel() {
|
||||||
|
const t = useTranslations("coupons");
|
||||||
|
const { addItem, clearCoupon } = useCouponStore();
|
||||||
|
|
||||||
|
const cardBg = useColorModeValue("white", "gray.800");
|
||||||
|
const mutedBg = useColorModeValue("gray.50", "whiteAlpha.50");
|
||||||
|
const borderColor = useColorModeValue("gray.200", "gray.700");
|
||||||
|
|
||||||
|
const freqMutation = useGenerateFrequencyCoupon();
|
||||||
|
|
||||||
|
const [minSignal, setMinSignal] = React.useState(0.65);
|
||||||
|
const [maxMatches, setMaxMatches] = React.useState(3);
|
||||||
|
const [selectedMarkets, setSelectedMarkets] = React.useState<string[]>([]);
|
||||||
|
const [result, setResult] = React.useState<
|
||||||
|
FrequencyCouponResultDto | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
const toggleMarket = (m: string) =>
|
||||||
|
setSelectedMarkets((prev) =>
|
||||||
|
prev.includes(m) ? prev.filter((x) => x !== m) : [...prev, m],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleGenerate = () => {
|
||||||
|
freqMutation.mutate(
|
||||||
|
{
|
||||||
|
maxMatches,
|
||||||
|
minSignal,
|
||||||
|
markets: selectedMarkets.length > 0 ? selectedMarkets : undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: (response) => {
|
||||||
|
const data = (response as any)?.data ?? response;
|
||||||
|
setResult(data as FrequencyCouponResultDto);
|
||||||
|
// Sync to coupon store
|
||||||
|
if (data && Array.isArray((data as any).bets)) {
|
||||||
|
clearCoupon();
|
||||||
|
(data as FrequencyCouponResultDto).bets.forEach(
|
||||||
|
(bet: FrequencyCouponBetDto) =>
|
||||||
|
addItem({
|
||||||
|
matchId: bet.match_id,
|
||||||
|
matchName: bet.match_name,
|
||||||
|
market: bet.market,
|
||||||
|
pick: bet.pick,
|
||||||
|
odd: bet.odds,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: () => setResult(undefined),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const errorMessage =
|
||||||
|
freqMutation.error instanceof ApiError
|
||||||
|
? freqMutation.error.message
|
||||||
|
: freqMutation.error instanceof Error
|
||||||
|
? freqMutation.error.message
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack gap={4} align="stretch">
|
||||||
|
{/* Controls Card */}
|
||||||
|
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="2xl">
|
||||||
|
<Card.Header>
|
||||||
|
<HStack gap={2}>
|
||||||
|
<Icon as={LuDatabase} color="cyan.500" boxSize={4.5} />
|
||||||
|
<Heading as="h3" size="sm">
|
||||||
|
{t("freq-engine-title")}
|
||||||
|
</Heading>
|
||||||
|
<InfoIcon
|
||||||
|
label={t("freq-engine-title")}
|
||||||
|
content={t("freq-engine-subtitle")}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
<Text fontSize="sm" color="fg.muted" mt={1}>
|
||||||
|
{t("freq-engine-subtitle")}
|
||||||
|
</Text>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Body pt={0}>
|
||||||
|
{/* Min Signal Slider */}
|
||||||
|
<VStack align="stretch" gap={2} mb={4}>
|
||||||
|
<HStack justify="space-between">
|
||||||
|
<HStack gap={2}>
|
||||||
|
<Icon as={LuTrendingUp} color="cyan.500" />
|
||||||
|
<Text fontWeight="semibold" fontSize="sm">
|
||||||
|
{t("freq-min-signal")}
|
||||||
|
</Text>
|
||||||
|
<InfoIcon
|
||||||
|
content={t("freq-min-signal-help")}
|
||||||
|
label={t("freq-min-signal")}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
<Badge colorPalette="cyan" variant="subtle">
|
||||||
|
{(minSignal * 100).toFixed(0)}%
|
||||||
|
</Badge>
|
||||||
|
</HStack>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="50"
|
||||||
|
max="95"
|
||||||
|
value={minSignal * 100}
|
||||||
|
onChange={(e) => setMinSignal(Number(e.target.value) / 100)}
|
||||||
|
style={{ width: "100%", accentColor: "#0891b2", cursor: "pointer" }}
|
||||||
|
/>
|
||||||
|
<HStack justify="space-between" fontSize="xs" color="fg.muted">
|
||||||
|
<Text>50%</Text>
|
||||||
|
<Text>95%</Text>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Max Matches */}
|
||||||
|
<VStack align="stretch" gap={2} mb={4}>
|
||||||
|
<HStack justify="space-between">
|
||||||
|
<HStack gap={2}>
|
||||||
|
<Icon as={LuBarChart3} color="purple.500" />
|
||||||
|
<Text fontWeight="semibold" fontSize="sm">
|
||||||
|
{t("match-count-label")}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<Badge colorPalette="purple" variant="subtle">
|
||||||
|
{maxMatches}
|
||||||
|
</Badge>
|
||||||
|
</HStack>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="2"
|
||||||
|
max="5"
|
||||||
|
value={maxMatches}
|
||||||
|
onChange={(e) => setMaxMatches(Number(e.target.value))}
|
||||||
|
style={{ width: "100%", accentColor: "#9333ea", cursor: "pointer" }}
|
||||||
|
/>
|
||||||
|
<HStack justify="space-between" fontSize="xs" color="fg.muted">
|
||||||
|
<Text>2</Text>
|
||||||
|
<Text>5</Text>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
<Separator mb={4} />
|
||||||
|
|
||||||
|
{/* Market Filter */}
|
||||||
|
<VStack align="stretch" gap={2} mb={4}>
|
||||||
|
<HStack gap={2}>
|
||||||
|
<Text fontWeight="semibold" fontSize="sm">
|
||||||
|
{t("freq-markets")}
|
||||||
|
</Text>
|
||||||
|
<InfoIcon
|
||||||
|
content={t("freq-markets-help")}
|
||||||
|
label={t("freq-markets")}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
<HStack gap={2} flexWrap="wrap">
|
||||||
|
{AVAILABLE_MARKETS.map((m) => {
|
||||||
|
const active = selectedMarkets.includes(m);
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
key={m}
|
||||||
|
colorPalette={active ? "cyan" : "gray"}
|
||||||
|
variant={active ? "solid" : "outline"}
|
||||||
|
cursor="pointer"
|
||||||
|
px={3}
|
||||||
|
py={1}
|
||||||
|
onClick={() => toggleMarket(m)}
|
||||||
|
_hover={{ opacity: 0.8 }}
|
||||||
|
>
|
||||||
|
{m}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</HStack>
|
||||||
|
{selectedMarkets.length === 0 && (
|
||||||
|
<Text fontSize="xs" color="fg.muted">
|
||||||
|
Tüm marketler taranacak
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
colorPalette="cyan"
|
||||||
|
size="lg"
|
||||||
|
width="full"
|
||||||
|
borderRadius="xl"
|
||||||
|
loading={freqMutation.isPending}
|
||||||
|
loadingText={t("freq-suggest-loading")}
|
||||||
|
onClick={handleGenerate}
|
||||||
|
>
|
||||||
|
<LuZap />
|
||||||
|
{t("freq-suggest")}
|
||||||
|
</Button>
|
||||||
|
</Card.Body>
|
||||||
|
</Card.Root>
|
||||||
|
|
||||||
|
{/* Results Card */}
|
||||||
|
{result && result.bets.length > 0 && (
|
||||||
|
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="2xl">
|
||||||
|
<Card.Header>
|
||||||
|
<HStack justify="space-between" align="center">
|
||||||
|
<HStack gap={2}>
|
||||||
|
<Icon as={LuZap} color="cyan.500" boxSize={4.5} />
|
||||||
|
<Heading as="h3" size="sm">
|
||||||
|
{t("freq-engine-title")}
|
||||||
|
</Heading>
|
||||||
|
</HStack>
|
||||||
|
<Badge
|
||||||
|
colorPalette={result.ev_positive ? "green" : "red"}
|
||||||
|
variant="solid"
|
||||||
|
>
|
||||||
|
{result.ev_positive
|
||||||
|
? t("freq-ev-positive")
|
||||||
|
: t("freq-ev-negative")}
|
||||||
|
</Badge>
|
||||||
|
</HStack>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Body pt={0}>
|
||||||
|
<VStack align="stretch" gap={3}>
|
||||||
|
{/* EV Stats */}
|
||||||
|
<Grid templateColumns="repeat(3, minmax(0, 1fr))" gap={3}>
|
||||||
|
<Box p={3} bg={mutedBg} borderRadius="xl">
|
||||||
|
<Text fontSize="xs" color="fg.muted" mb={1}>
|
||||||
|
{t("freq-ev-label")}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
fontWeight="bold"
|
||||||
|
fontSize="lg"
|
||||||
|
color={result.ev_positive ? "green.500" : "red.500"}
|
||||||
|
>
|
||||||
|
{result.expected_value.toFixed(3)}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box p={3} bg={mutedBg} borderRadius="xl">
|
||||||
|
<Text fontSize="xs" color="fg.muted" mb={1}>
|
||||||
|
{t("freq-hit-rate")}
|
||||||
|
</Text>
|
||||||
|
<Text fontWeight="bold" fontSize="lg" color="cyan.500">
|
||||||
|
{(result.expected_hit_rate * 100).toFixed(1)}%
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box p={3} bg={mutedBg} borderRadius="xl">
|
||||||
|
<Text fontSize="xs" color="fg.muted" mb={1}>
|
||||||
|
{t("total-odds")}
|
||||||
|
</Text>
|
||||||
|
<Text fontWeight="bold" fontSize="lg" color="purple.500">
|
||||||
|
{result.total_odds.toFixed(2)}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Bets */}
|
||||||
|
{result.bets.map((bet: FrequencyCouponBetDto) => (
|
||||||
|
<Box
|
||||||
|
key={`${bet.match_id}-${bet.market}`}
|
||||||
|
p={4}
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
borderRadius="xl"
|
||||||
|
bg={mutedBg}
|
||||||
|
>
|
||||||
|
<Flex justify="space-between" align="flex-start" gap={3} mb={3}>
|
||||||
|
<VStack align="flex-start" gap={1}>
|
||||||
|
<Text fontWeight="bold">{bet.match_name}</Text>
|
||||||
|
<Text fontSize="sm" color="fg.muted">
|
||||||
|
{bet.league} • {bet.market}: {bet.pick}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
<Badge colorPalette="cyan" variant="solid">
|
||||||
|
{bet.odds.toFixed(2)}
|
||||||
|
</Badge>
|
||||||
|
</Flex>
|
||||||
|
<Grid templateColumns="repeat(3, minmax(0, 1fr))" gap={2}>
|
||||||
|
<Box>
|
||||||
|
<Text fontSize="xs" color="fg.muted" mb={1}>
|
||||||
|
{t("freq-home-signal")}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="sm" fontWeight="semibold">
|
||||||
|
{(bet.home_signal * 100).toFixed(0)}%
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="xs" color="fg.muted">
|
||||||
|
{bet.home_odds_band}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fontSize="xs" color="fg.muted" mb={1}>
|
||||||
|
{t("freq-away-signal")}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="sm" fontWeight="semibold">
|
||||||
|
{(bet.away_signal * 100).toFixed(0)}%
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="xs" color="fg.muted">
|
||||||
|
{bet.away_odds_band}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fontSize="xs" color="fg.muted" mb={1}>
|
||||||
|
{t("freq-combined-signal")}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="sm" fontWeight="bold" color="cyan.500">
|
||||||
|
{(bet.combined_signal * 100).toFixed(0)}%
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<HStack gap={2} mt={3} flexWrap="wrap">
|
||||||
|
<Badge
|
||||||
|
colorPalette={profileColor(bet.league_profile)}
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
{t("freq-league-profile")}:{" "}
|
||||||
|
{profileLabel(bet.league_profile, t)}
|
||||||
|
</Badge>
|
||||||
|
<Badge colorPalette="gray" variant="subtle">
|
||||||
|
{t("freq-match-count")}: {bet.home_match_count}/
|
||||||
|
{bet.away_match_count}
|
||||||
|
</Badge>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Reasoning */}
|
||||||
|
{result.reasoning.length > 0 && (
|
||||||
|
<Box p={4} bg={mutedBg} borderRadius="xl">
|
||||||
|
<Text fontWeight="semibold" fontSize="sm" mb={2}>
|
||||||
|
{t("freq-reasoning-title")}
|
||||||
|
</Text>
|
||||||
|
<VStack align="stretch" gap={1}>
|
||||||
|
{result.reasoning.map((r, i) => (
|
||||||
|
<Text key={i} fontSize="xs" color="fg.muted">
|
||||||
|
• {r}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Rejected */}
|
||||||
|
{result.rejected_matches.length > 0 && (
|
||||||
|
<Box p={4} bg="orange.50" borderRadius="xl">
|
||||||
|
<HStack gap={2} mb={2}>
|
||||||
|
<Icon as={LuBadgeAlert} color="orange.500" />
|
||||||
|
<Text fontWeight="semibold">{t("rejected-matches-title")}</Text>
|
||||||
|
</HStack>
|
||||||
|
<VStack align="stretch" gap={1}>
|
||||||
|
{result.rejected_matches.map((entry, i) => (
|
||||||
|
<Text key={i} fontSize="sm" color="fg.muted">
|
||||||
|
{entry.match_name}: {entry.reason}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</Card.Body>
|
||||||
|
</Card.Root>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* No result message */}
|
||||||
|
{result && result.bets.length === 0 && (
|
||||||
|
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="2xl">
|
||||||
|
<Card.Body py={8}>
|
||||||
|
<Text textAlign="center" color="fg.muted">
|
||||||
|
{t("freq-no-result")}
|
||||||
|
</Text>
|
||||||
|
</Card.Body>
|
||||||
|
</Card.Root>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error */}
|
||||||
|
{errorMessage && (
|
||||||
|
<Box
|
||||||
|
p={4}
|
||||||
|
bg="red.50"
|
||||||
|
borderRadius="xl"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor="red.200"
|
||||||
|
>
|
||||||
|
<Text fontSize="sm" color="red.700">
|
||||||
|
{errorMessage}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
MatchAnalysisResultDto,
|
MatchAnalysisResultDto,
|
||||||
DailyBankoResponseDto,
|
DailyBankoResponseDto,
|
||||||
SmartCouponResultDto,
|
SmartCouponResultDto,
|
||||||
|
FrequencyCouponRequestDto,
|
||||||
|
FrequencyCouponResultDto,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,6 +72,15 @@ const suggestCoupon = (dto: SuggestCouponDto) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const generateFrequencyCoupon = (dto: FrequencyCouponRequestDto) => {
|
||||||
|
return apiRequest<ApiResponse<FrequencyCouponResultDto>>({
|
||||||
|
url: "/coupon/frequency-coupon",
|
||||||
|
client: "core",
|
||||||
|
method: "post",
|
||||||
|
data: dto,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const couponsService = {
|
export const couponsService = {
|
||||||
analyzeMatch,
|
analyzeMatch,
|
||||||
createCoupon,
|
createCoupon,
|
||||||
@@ -77,4 +88,6 @@ export const couponsService = {
|
|||||||
getHistory,
|
getHistory,
|
||||||
getUserStats,
|
getUserStats,
|
||||||
suggestCoupon,
|
suggestCoupon,
|
||||||
|
generateFrequencyCoupon,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -106,3 +106,50 @@ export interface SmartCouponResultDto {
|
|||||||
expected_win_rate: number;
|
expected_win_rate: number;
|
||||||
rejected_matches: SuggestedCouponRejectedMatchDto[];
|
rejected_matches: SuggestedCouponRejectedMatchDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================
|
||||||
|
// Frequency Engine DTOs
|
||||||
|
// ========================
|
||||||
|
|
||||||
|
export interface FrequencyCouponRequestDto {
|
||||||
|
matchIds?: string[];
|
||||||
|
maxMatches?: number;
|
||||||
|
minSignal?: number;
|
||||||
|
markets?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FrequencyCouponBetDto {
|
||||||
|
match_id: string;
|
||||||
|
match_name: string;
|
||||||
|
league: string;
|
||||||
|
market: string;
|
||||||
|
pick: string;
|
||||||
|
home_signal: number;
|
||||||
|
away_signal: number;
|
||||||
|
combined_signal: number;
|
||||||
|
league_profile: string;
|
||||||
|
historical_hit_rate: number;
|
||||||
|
odds: number;
|
||||||
|
home_odds_band: string;
|
||||||
|
away_odds_band: string;
|
||||||
|
home_match_count: number;
|
||||||
|
away_match_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FrequencyCouponRejectedDto {
|
||||||
|
match_id: string;
|
||||||
|
match_name: string;
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FrequencyCouponResultDto {
|
||||||
|
strategy: "FREQUENCY";
|
||||||
|
generated_at: string;
|
||||||
|
bets: FrequencyCouponBetDto[];
|
||||||
|
total_odds: number;
|
||||||
|
expected_hit_rate: number;
|
||||||
|
expected_value: number;
|
||||||
|
ev_positive: boolean;
|
||||||
|
reasoning: string[];
|
||||||
|
rejected_matches: FrequencyCouponRejectedDto[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
CreateCouponDto,
|
CreateCouponDto,
|
||||||
SuggestCouponDto,
|
SuggestCouponDto,
|
||||||
AnalyzeMatchDto,
|
AnalyzeMatchDto,
|
||||||
|
FrequencyCouponRequestDto,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const CouponsQueryKeys = {
|
export const CouponsQueryKeys = {
|
||||||
@@ -55,3 +56,11 @@ export const useSuggestCoupon = () => {
|
|||||||
mutationFn: (dto: SuggestCouponDto) => couponsService.suggestCoupon(dto),
|
mutationFn: (dto: SuggestCouponDto) => couponsService.suggestCoupon(dto),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGenerateFrequencyCoupon = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (dto: FrequencyCouponRequestDto) =>
|
||||||
|
couponsService.generateFrequencyCoupon(dto),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user