Initial commit with 2026 World Cup Quant Platform core modules and CI/CD

This commit is contained in:
QuantBot
2026-06-13 23:18:18 +08:00
commit 073abf98c1
155 changed files with 19539 additions and 0 deletions

View File

@@ -0,0 +1,262 @@
'use client';
import { useState } from 'react';
import {
analyzeMlEdge,
type MlEdgeRequestPayload,
type MlEdgeResponse,
type MlTrainRequestPayload,
type MlTrainResponse,
type MlTrainingRow,
trainMlModel,
} from '@/lib/analytics-api';
const defaultTrainingRows: MlTrainingRow[] = [
{ home_rest_days: 4, away_rest_days: 3, home_travel_distance_km: 520, away_travel_distance_km: 1100, recent_5_xg_home: 1.8, recent_5_xg_away: 1.0, match_result: 'home' },
{ home_rest_days: 2, away_rest_days: 5, home_travel_distance_km: 220, away_travel_distance_km: 780, recent_5_xg_home: 1.1, recent_5_xg_away: 1.7, match_result: 'away' },
{ home_rest_days: 6, away_rest_days: 4, home_travel_distance_km: 120, away_travel_distance_km: 960, recent_5_xg_home: 2.3, recent_5_xg_away: 1.8, match_result: 'home' },
{ home_rest_days: 3, away_rest_days: 3, home_travel_distance_km: 900, away_travel_distance_km: 900, recent_5_xg_home: 1.2, recent_5_xg_away: 1.3, match_result: 'draw' },
];
export default function MlEdgePage() {
const [modelId, setModelId] = useState('default');
const [matchId, setMatchId] = useState('MATCH-2026-FINALS-07');
const [homeRest, setHomeRest] = useState(6);
const [awayRest, setAwayRest] = useState(4);
const [homeTravel, setHomeTravel] = useState(420);
const [awayTravel, setAwayTravel] = useState(970);
const [homeXg, setHomeXg] = useState(2.0);
const [awayXg, setAwayXg] = useState(1.1);
const [homeOdds, setHomeOdds] = useState(2.05);
const [drawOdds, setDrawOdds] = useState(3.35);
const [awayOdds, setAwayOdds] = useState(3.75);
const [loading, setLoading] = useState(false);
const [trainLoading, setTrainLoading] = useState(false);
const [edgeResult, setEdgeResult] = useState<MlEdgeResponse | null>(null);
const [trainResult, setTrainResult] = useState<MlTrainResponse | null>(null);
const [errorMessage, setErrorMessage] = useState('');
const [trainMessage, setTrainMessage] = useState('');
async function runEdge() {
setLoading(true);
setErrorMessage('');
try {
const payload: MlEdgeRequestPayload = {
model_id: modelId,
match_id: matchId,
home_rest_days: homeRest,
away_rest_days: awayRest,
home_travel_distance_km: homeTravel,
away_travel_distance_km: awayTravel,
recent_5_xg_home: homeXg,
recent_5_xg_away: awayXg,
home_implied_odds: homeOdds,
draw_implied_odds: drawOdds,
away_implied_odds: awayOdds,
};
const data = await analyzeMlEdge(payload);
setEdgeResult(data);
setModelId(data.model_id);
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : 'ML 邊界分析暫時失敗');
setEdgeResult(null);
} finally {
setLoading(false);
}
}
async function trainModel() {
setTrainLoading(true);
setTrainMessage('');
try {
const payload: MlTrainRequestPayload = {
model_id: `model-${Date.now()}`,
rows: defaultTrainingRows,
};
const result = await trainMlModel(payload);
setTrainResult(result);
setModelId(result.model_id);
setTrainMessage(`訓練完成(${result.training_size}筆)`);
} catch (error) {
setTrainMessage(error instanceof Error ? error.message : '模型訓練失敗');
} finally {
setTrainLoading(false);
}
}
return (
<div className="space-y-4">
<h2 className="dot-matrix text-2xl text-[#7d2a15]">ML Ensemble 15 </h2>
<section className="panel-glow rounded-2xl p-4">
<h3 className="dot-matrix text-lg text-[#7d2a15]"></h3>
<div className="mt-3 grid gap-3 md:grid-cols-2 lg:grid-cols-3">
<label className="text-sm text-[#7a5b46]">
使 ID
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
value={modelId}
onChange={(event) => setModelId(event.target.value)}
/>
</label>
<label className="text-sm text-[#7a5b46]">
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
value={matchId}
onChange={(event) => setMatchId(event.target.value)}
/>
</label>
<label className="text-sm text-[#7a5b46]">
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
type="number"
value={homeRest}
onChange={(event) => setHomeRest(Number(event.target.value))}
/>
</label>
<label className="text-sm text-[#7a5b46]">
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
type="number"
value={awayRest}
onChange={(event) => setAwayRest(Number(event.target.value))}
/>
</label>
<label className="text-sm text-[#7a5b46]">
km
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
type="number"
value={homeTravel}
onChange={(event) => setHomeTravel(Number(event.target.value))}
/>
</label>
<label className="text-sm text-[#7a5b46]">
km
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
type="number"
value={awayTravel}
onChange={(event) => setAwayTravel(Number(event.target.value))}
/>
</label>
<label className="text-sm text-[#7a5b46]">
xG
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
type="number"
step={0.1}
value={homeXg}
onChange={(event) => setHomeXg(Number(event.target.value))}
/>
</label>
<label className="text-sm text-[#7a5b46]">
xG
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
type="number"
step={0.1}
value={awayXg}
onChange={(event) => setAwayXg(Number(event.target.value))}
/>
</label>
<label className="text-sm text-[#7a5b46]">
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
type="number"
step={0.01}
value={homeOdds}
onChange={(event) => setHomeOdds(Number(event.target.value))}
/>
</label>
<label className="text-sm text-[#7a5b46]">
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
type="number"
step={0.01}
value={drawOdds}
onChange={(event) => setDrawOdds(Number(event.target.value))}
/>
</label>
<label className="text-sm text-[#7a5b46]">
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
type="number"
step={0.01}
value={awayOdds}
onChange={(event) => setAwayOdds(Number(event.target.value))}
/>
</label>
</div>
<div className="mt-4 flex flex-wrap gap-3">
<button
className="rounded-lg bg-[#7d2a15] px-4 py-2 text-white"
type="button"
onClick={runEdge}
disabled={loading}
>
{loading ? '分析中…' : '執行 ML Edge 推論'}
</button>
<button
className="rounded-lg bg-[#d1432d] px-4 py-2 text-white"
type="button"
onClick={trainModel}
disabled={trainLoading}
>
{trainLoading ? '訓練中…' : '快速訓練演示模型'}
</button>
</div>
{errorMessage ? <p className="mt-3 text-xs text-[#8c2f2f]">{errorMessage}</p> : null}
{trainMessage ? <p className="mt-2 text-xs text-[#6f4f3c]">{trainMessage}</p> : null}
</section>
<section className="panel-glow rounded-2xl p-4">
<h3 className="dot-matrix text-lg text-[#7d2a15]"></h3>
{edgeResult ? (
<div className="mt-3 grid gap-3 md:grid-cols-2">
<article className="rounded-xl border border-[#e7c89b] bg-white/70 p-4">
<p className="text-sm text-[#7a5b46]"></p>
<p className="mt-1 text-sm text-[#5f4330]"> {edgeResult.model_probs.home.toFixed(2)} / {edgeResult.model_probs.draw.toFixed(2)} / {edgeResult.model_probs.away.toFixed(2)}</p>
<p className="mt-3 text-xs text-[#6f4f3c]">{edgeResult.is_fallback_model ? '規則回退' : 'ML Ensemble'}</p>
</article>
<article className="rounded-xl border border-[#e7c89b] bg-white/70 p-4">
<p className="text-sm text-[#7a5b46]"></p>
<p className="mt-1 text-2xl font-semibold text-[#b83822]">{edgeResult.strongest_outcome}</p>
<p className="text-sm text-[#5f4330]">Edge{edgeResult.strongest_edge_percent.toFixed(2)}%</p>
<p className="mt-2 text-xs text-[#7a5b46]"> Strong Buy{edgeResult.strong_buy ? '是' : '否'}</p>
</article>
<article className="rounded-xl border border-[#e7c89b] bg-white/70 p-4 md:col-span-2">
<p className="text-sm text-[#7a5b46]"> Edge </p>
<ul className="mt-2 space-y-1 text-sm text-[#5f4330]">
<li>{(edgeResult.edges.home.edge * 100).toFixed(2)}% {edgeResult.edges.home.strong_buy ? 'Strong Buy' : ''}</li>
<li>{(edgeResult.edges.draw.edge * 100).toFixed(2)}% {edgeResult.edges.draw.strong_buy ? 'Strong Buy' : ''}</li>
<li>{(edgeResult.edges.away.edge * 100).toFixed(2)}% {edgeResult.edges.away.strong_buy ? 'Strong Buy' : ''}</li>
</ul>
</article>
</div>
) : (
<p className="mt-3 text-sm text-[#7a5b46]"> ML Edge </p>
)}
</section>
<section className="panel-glow rounded-2xl p-4">
<h3 className="dot-matrix text-lg text-[#7d2a15]"></h3>
{trainResult ? (
<p className="mt-2 text-sm text-[#7a5b46]">
{trainResult.model_id} | {trainResult.training_size} |
{trainResult.accuracy === null ? 'N/A' : trainResult.accuracy.toFixed(4)} | 退{trainResult.is_fallback ? '是' : '否'}
</p>
) : (
<p className="mt-2 text-sm text-[#7a5b46]"></p>
)}
</section>
</div>
);
}