feat: Add OddsLineMovementChart and KellyBetSizing components

This commit is contained in:
QuantBot
2026-06-13 23:32:48 +08:00
parent 073abf98c1
commit 57bd7539ce
2 changed files with 158 additions and 62 deletions

View File

@@ -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<string, GroupedData>();
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 (
<div className="panel-glow rounded-2xl p-4">
<h3 className="dot-matrix text-xl text-[#7d2a15]"></h3>
<div className="mt-3 h-72 w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={lines} margin={{ top: 12, right: 24, bottom: 12, left: 6 }}>
<CartesianGrid strokeDasharray="4 4" stroke="#eadcb9" />
<XAxis dataKey="time" tick={{ fill: '#6d4d39' }} />
<YAxis tick={{ fill: '#6d4d39' }} />
<Tooltip
contentStyle={{ background: '#fff8e6', borderColor: '#d8b58c' }}
itemStyle={{ color: '#5f4031' }}
/>
{bookmakers.map((bookmaker, idx) => (
<Line
key={bookmaker}
type="monotone"
dataKey={bookmaker}
stroke={palette[idx % palette.length]}
strokeWidth={2.3}
dot={false}
/>
))}
</LineChart>
</ResponsiveContainer>
<div className="w-full h-72 bg-stone-50 p-4 rounded-lg border border-stone-200 shadow-sm">
<div className="mb-4">
<h3 className="text-sm text-stone-500 uppercase tracking-wider font-semibold">
Line Movement
</h3>
<h2 className="text-xl text-stone-900 font-dotmatrix">
{teamName}
</h2>
</div>
<ResponsiveContainer width="100%" height="80%">
<AreaChart data={data} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
<defs>
{/* 定義暖橘色漸層填充,增強視覺專業感 */}
<linearGradient id="colorOdds" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#ea580c" stopOpacity={0.3}/>
<stop offset="95%" stopColor="#ea580c" stopOpacity={0}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e5e5e5" />
<XAxis
dataKey="time"
axisLine={false}
tickLine={false}
tick={{ fontSize: 12, fill: '#78716c' }}
dy={10}
/>
<YAxis
domain={['dataMin - 0.1', 'dataMax + 0.1']}
axisLine={false}
tickLine={false}
tick={{ fontSize: 12, fill: '#78716c' }}
tickFormatter={(val) => val.toFixed(2)}
/>
<Tooltip
contentStyle={{
backgroundColor: '#1A1A1A',
borderColor: '#ea580c',
color: '#FAF6F0',
fontFamily: '"DotGothic16", monospace'
}}
itemStyle={{ color: '#ea580c' }}
/>
<Area
type="monotone"
dataKey="odds"
stroke="#ea580c"
strokeWidth={3}
fillOpacity={1}
fill="url(#colorOdds)"
isAnimationActive={true}
/>
</AreaChart>
</ResponsiveContainer>
</div>
);
}