Files
2026FIFAWorldCup/platform/web/app/proof-of-yield/page.tsx
QuantBot aa7e3bba76
Some checks failed
2026 World Cup Quant Platform - Production Deployment / Code Quality & Testing (push) Failing after 1m49s
2026 World Cup Quant Platform - Production Deployment / Deploy to Production VM via Rsync (push) Has been skipped
chore: migrate deployment to Gitea Actions with zero-trust rsync
2026-06-16 19:06:50 +08:00

139 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import React, { useEffect, useState } from 'react';
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { getProofOfYieldLedger, type ProofOfYieldLedgerResponse } from '@/lib/analytics-api';
export default function ProofOfYieldPage() {
const [data, setData] = useState<ProofOfYieldLedgerResponse | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function loadData() {
try {
const ledger = await getProofOfYieldLedger(200);
setData(ledger);
} catch (error) {
console.error('Failed to load proof of yield data', error);
} finally {
setLoading(false);
}
}
loadData();
}, []);
if (loading) {
return <div className="p-8 text-[#8a6b58] dot-matrix">...</div>;
}
// 避免無資料時崩潰
const summary = data?.summary || {
roi_percent: 0,
win_rate_percent: 0,
avg_clv_percent: 0
};
const records = data?.records || [];
// 如果後端目前無真實獲利資料,產生一個平穩的空曲線
const equityData = records.length > 0 ? records.map((r, i) => ({
date: r.settled_at.split('T')[0],
equity: 10000 + (r.pnl || 0) * (i + 1) // 簡化處理
})) : [
{ date: new Date().toISOString().split('T')[0], equity: 10000 }
];
return (
<div className="min-h-screen bg-stone-50 p-8">
<div className="max-w-6xl mx-auto">
{/* 標題與核心指標 */}
<div className="mb-10">
<h1 className="text-3xl font-bold text-stone-900 uppercase tracking-widest border-b-2 border-quant-orange pb-2 inline-block">
<span className="text-stone-400 text-lg ml-2"></span>
</h1>
<p className="text-stone-500 mt-2"></p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
<div className="bg-white p-6 rounded-lg border border-stone-200 shadow-sm">
<p className="text-sm text-stone-500 font-semibold"></p>
<p className={`text-4xl font-dotmatrix mt-2 ${summary.roi_percent >= 0 ? 'text-quant-orange' : 'text-red-500'}`}>
{summary.roi_percent > 0 ? '+' : ''}{summary.roi_percent.toFixed(2)}%
</p>
</div>
<div className="bg-white p-6 rounded-lg border border-stone-200 shadow-sm">
<p className="text-sm text-stone-500 uppercase font-semibold">Win Rate ()</p>
<p className="text-4xl text-stone-800 font-dotmatrix mt-2">{summary.win_rate_percent.toFixed(2)}%</p>
</div>
<div className="bg-white p-6 rounded-lg border border-stone-200 shadow-sm relative overflow-hidden">
<div className="absolute right-0 top-0 w-2 h-full bg-quant-red"></div>
<p className="text-sm text-stone-500 font-semibold"></p>
<p className={`text-4xl font-dotmatrix mt-2 ${summary.avg_clv_percent >= 0 ? 'text-quant-red' : 'text-red-500'}`}>
{summary.avg_clv_percent > 0 ? '+' : ''}{summary.avg_clv_percent.toFixed(2)}%
</p>
<p className="text-xs text-stone-400 mt-1"></p>
</div>
</div>
{/* 資金成長曲線 */}
<div className="bg-white p-6 rounded-lg border border-stone-200 shadow-sm mb-10 h-96">
<h3 className="text-lg font-bold text-stone-800 mb-6"></h3>
{records.length === 0 ? (
<div className="flex h-full items-center justify-center text-sm text-stone-500">
</div>
) : (
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={equityData} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="colorEquity" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#ea580c" stopOpacity={0.4}/>
<stop offset="95%" stopColor="#ea580c" stopOpacity={0}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f5f5f4" />
<XAxis dataKey="date" axisLine={false} tickLine={false} tick={{ fontSize: 12, fill: '#a8a29e' }} />
<YAxis domain={['auto', 'auto']} axisLine={false} tickLine={false} tick={{ fontSize: 12, fill: '#a8a29e' }} />
<Tooltip contentStyle={{ backgroundColor: '#1A1A1A', borderColor: '#ea580c', color: '#FAF6F0', fontFamily: '"DotGothic16", monospace' }} />
<Area type="monotone" dataKey="equity" stroke="#ea580c" strokeWidth={3} fillOpacity={1} fill="url(#colorEquity)" />
</AreaChart>
</ResponsiveContainer>
)}
</div>
{/* 歷史注單明細表 */}
<div className="bg-white rounded-lg border border-stone-200 shadow-sm overflow-hidden">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-stone-100 text-stone-600 text-sm uppercase tracking-wider">
<th className="p-4 font-semibold"></th>
<th className="p-4 font-semibold"> / </th>
<th className="p-4 font-semibold"></th>
<th className="p-4 font-semibold"></th>
<th className="p-4 font-semibold"></th>
</tr>
</thead>
<tbody className="divide-y divide-stone-200 font-dotmatrix text-lg">
{records.length === 0 ? (
<tr>
<td colSpan={5} className="p-4 text-center text-stone-500"></td>
</tr>
) : records.map((bet) => (
<tr key={bet.recommendation_id} className="hover:bg-stone-50 transition-colors">
<td className="p-4 text-stone-500">{bet.settled_at.split('T')[0]}</td>
<td className="p-4 text-stone-800 font-sans font-medium">{bet.match_id} <span className="text-stone-400 mx-2">|</span> <span className="text-quant-orange">{bet.selection}</span></td>
<td className="p-4 text-quant-red">{bet.clv_percent !== null ? `${bet.clv_percent.toFixed(2)}%` : '-'}</td>
<td className={`p-4 font-bold ${bet.is_win ? 'text-green-600' : 'text-stone-400'}`}>{bet.is_win ? '命中' : '未中'}</td>
<td className={`p-4 ${bet.pnl > 0 ? 'text-green-600' : 'text-red-500'}`}>{bet.pnl > 0 ? '+' : ''}{bet.pnl.toFixed(2)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}