diff --git a/platform/web/components/KellyBetSizing.tsx b/platform/web/components/KellyBetSizing.tsx new file mode 100644 index 0000000..6d98034 --- /dev/null +++ b/platform/web/components/KellyBetSizing.tsx @@ -0,0 +1,90 @@ +'use client'; + +import React, { useState, useMemo } from 'react'; + +interface KellyBetSizingProps { + trueProb: number; // 系統計算的真實勝率 (0.0 - 1.0) + decimalOdds: number; // 莊家賠率 +} + +export default function KellyBetSizing({ trueProb, decimalOdds }: KellyBetSizingProps) { + const [bankroll, setBankroll] = useState(10000); + const [kellyFraction, setKellyFraction] = useState(0.25); // 預設 1/4 凱利 + + // 核心計算邏輯 (套用凱利公式) + const suggestedStake = useMemo(() => { + const b = decimalOdds - 1.0; + const p = trueProb; + const q = 1.0 - p; + + if (b <= 0) return 0; + + const fullKellyPct = ((b * p) - q) / b; + if (fullKellyPct <= 0) return 0; + + return Math.round(bankroll * fullKellyPct * kellyFraction); + }, [bankroll, kellyFraction, decimalOdds, trueProb]); + + // 動態樣式判斷 + const isHighRisk = kellyFraction > 0.5; + const highlightColor = isHighRisk ? 'text-quant-red' : 'text-quant-orange'; + + return ( +
+
+
+

+ Kelly Criterion +

+

量化注碼建議

+
+
+

建議絕對金額

+

+ $ {suggestedStake.toLocaleString()} +

+
+
+ + {/* 控制區:總資金 */} +
+ + setBankroll(Number(e.target.value))} + className="w-full h-2 bg-stone-200 rounded-lg appearance-none cursor-pointer accent-quant-orange" + /> +
+ + {/* 控制區:風險偏好 (分數凱利) */} +
+ + setKellyFraction(Number(e.target.value))} + className="w-full h-2 bg-stone-200 rounded-lg appearance-none cursor-pointer accent-stone-700" + /> +
+ Conservative (0.1x) + Optimal (0.25x) + Full Risk (1.0x) +
+
+
+ ); +} diff --git a/platform/web/components/OddsLineMovementChart.tsx b/platform/web/components/OddsLineMovementChart.tsx index 8a3ec3a..68b752f 100644 --- a/platform/web/components/OddsLineMovementChart.tsx +++ b/platform/web/components/OddsLineMovementChart.tsx @@ -1,77 +1,83 @@ 'use client'; +import React from 'react'; import { - CartesianGrid, - Line, - LineChart, - ResponsiveContainer, - Tooltip, + AreaChart, + Area, XAxis, YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer } from 'recharts'; -type ChartPoint = { - time: string; - odds: number; - bookmaker: string; -}; - -type GroupedData = { - time: string; - [bookmaker: string]: string | number; -}; - -function normalizeForChart(points: ChartPoint[]): GroupedData[] { - const map = new Map(); - - for (const row of points) { - const current = map.get(row.time) || { time: row.time }; - current[row.bookmaker] = row.odds; - map.set(row.time, current); - } - - return Array.from(map.values()).sort((a, b) => - a.time.localeCompare(b.time), - ); +// 定義傳入組件的歷史賠率資料介面 +export interface OddsDataPoint { + time: string; // 格式化後的時間 (如: 14:30) + odds: number; // 小數賠率 } -type Props = { - data: ChartPoint[]; -}; - -export function OddsLineMovementChart({ data }: Props) { - const bookmakers = Array.from(new Set(data.map((row) => row.bookmaker))); - const lines = normalizeForChart(data); - - const palette = ['#b83822', '#7a4a2c', '#dcb53b', '#5f4031', '#8c5b38']; +interface OddsLineMovementChartProps { + data: OddsDataPoint[]; + teamName: string; +} +export default function OddsLineMovementChart({ data, teamName }: OddsLineMovementChartProps) { return ( -
-

賠率走勢:小數賠率變化

-
- - - - - - - {bookmakers.map((bookmaker, idx) => ( - - ))} - - +
+
+

+ Line Movement +

+

+ {teamName} 賠率走勢 +

+ + + + + {/* 定義暖橘色漸層填充,增強視覺專業感 */} + + + + + + + + val.toFixed(2)} + /> + + + +
); } -