Initial commit with 2026 World Cup Quant Platform core modules and CI/CD
This commit is contained in:
262
platform/web/app/ml-edge/page.tsx
Normal file
262
platform/web/app/ml-edge/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user