156 lines
6.1 KiB
TypeScript
156 lines
6.1 KiB
TypeScript
import { useMemo } from 'react';
|
||
import {
|
||
Area,
|
||
AreaChart,
|
||
CartesianGrid,
|
||
ResponsiveContainer,
|
||
Tooltip,
|
||
XAxis,
|
||
YAxis,
|
||
} from 'recharts';
|
||
|
||
import type { ProofOfYieldRecord, ProofOfYieldSummary } from '@/lib/analytics-api';
|
||
|
||
type Point = {
|
||
ts: string;
|
||
cumulative: number;
|
||
pnl: number;
|
||
};
|
||
|
||
type PerformanceLedgerProps = {
|
||
summary: ProofOfYieldSummary;
|
||
records: ProofOfYieldRecord[];
|
||
};
|
||
|
||
export function PerformanceLedger({ summary, records }: PerformanceLedgerProps) {
|
||
const sorted = useMemo(
|
||
() => [...records].sort((a, b) => new Date(a.settled_at).getTime() - new Date(b.settled_at).getTime()),
|
||
[records],
|
||
);
|
||
|
||
const curve: Point[] = useMemo(() => {
|
||
let sum = 0;
|
||
return sorted.map((record) => {
|
||
sum += record.pnl;
|
||
return {
|
||
ts: record.settled_at.slice(0, 16).replace('T', ' '),
|
||
cumulative: Number(sum.toFixed(4)),
|
||
pnl: record.pnl,
|
||
};
|
||
});
|
||
}, [sorted]);
|
||
|
||
const maxClv = useMemo(() => {
|
||
const values = records.map((record) => record.clv_percent).filter((value) => value !== null) as number[];
|
||
if (values.length === 0) {
|
||
return 0;
|
||
}
|
||
return Math.max(...values);
|
||
}, [records]);
|
||
|
||
return (
|
||
<section className="space-y-4">
|
||
<section className="grid gap-3 md:grid-cols-4">
|
||
<article className="panel-glow rounded-xl p-3">
|
||
<p className="text-xs text-[#7a5b46]">推薦筆數</p>
|
||
<p className="mt-2 text-2xl font-semibold text-[#7d2a15]">{summary.total_recommendations}</p>
|
||
</article>
|
||
<article className="panel-glow rounded-xl p-3">
|
||
<p className="text-xs text-[#7a5b46]">命中率</p>
|
||
<p className="mt-2 text-2xl font-semibold text-[#7d2a15]">{summary.win_rate_percent.toFixed(1)}%</p>
|
||
</article>
|
||
<article className="panel-glow rounded-xl p-3">
|
||
<p className="text-xs text-[#7a5b46]">報酬率</p>
|
||
<p className={`mt-2 text-2xl font-semibold ${summary.roi_percent >= 10 ? 'text-[#d1432d]' : 'text-[#7d2a15]'}`}>
|
||
{summary.roi_percent.toFixed(2)}%
|
||
</p>
|
||
</article>
|
||
<article className="panel-glow rounded-xl p-3">
|
||
<p className="text-xs text-[#7a5b46]">平均收盤價差</p>
|
||
<p className="mt-2 dot-matrix text-2xl font-semibold text-[#7d2a15]">
|
||
{summary.avg_clv_percent.toFixed(2)}%
|
||
</p>
|
||
</article>
|
||
</section>
|
||
|
||
<article className="panel-glow rounded-2xl p-4">
|
||
<h3 className="dot-matrix text-lg text-[#7d2a15]">公開建議資金成長曲線</h3>
|
||
<div className="mt-3 h-72 w-full">
|
||
<ResponsiveContainer width="100%" height="100%">
|
||
<AreaChart data={curve} margin={{ top: 6, right: 18, bottom: 6, left: 8 }}>
|
||
<defs>
|
||
<linearGradient id="ledgerGradient" x1="0" y1="0" x2="0" y2="1">
|
||
<stop offset="0%" stopColor="#d1432d" stopOpacity={0.45} />
|
||
<stop offset="100%" stopColor="#d1432d" stopOpacity={0.03} />
|
||
</linearGradient>
|
||
</defs>
|
||
<CartesianGrid strokeDasharray="4 4" stroke="#eadcb9" />
|
||
<XAxis dataKey="ts" tick={{ fill: '#6d4d39', fontSize: 11 }} />
|
||
<YAxis tick={{ fill: '#6d4d39', fontSize: 11 }} />
|
||
<Tooltip
|
||
contentStyle={{ background: '#fff8e6', borderColor: '#d8b58c' }}
|
||
itemStyle={{ color: '#5f4031' }}
|
||
/>
|
||
<Area
|
||
type="monotone"
|
||
dataKey="cumulative"
|
||
stroke="#b83822"
|
||
fill="url(#ledgerGradient)"
|
||
strokeWidth={2.4}
|
||
/>
|
||
</AreaChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
<p className="mt-2 text-xs text-[#8c2f2f]">歷史最佳收盤價差:{maxClv.toFixed(2)}%</p>
|
||
</article>
|
||
|
||
<article className="panel-glow rounded-2xl p-4">
|
||
<h3 className="dot-matrix text-lg text-[#7d2a15]">建議明細(公開透明)</h3>
|
||
<div className="mt-3 overflow-x-auto">
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="border-b border-[#e6cfad] text-[#7a5b46]">
|
||
<th className="py-2 text-left">比賽</th>
|
||
<th className="py-2 text-left">市場</th>
|
||
<th className="py-2 text-left">選項</th>
|
||
<th className="py-2 text-left">投入</th>
|
||
<th className="py-2 text-left">推薦賠率</th>
|
||
<th className="py-2 text-left">收盤賠率</th>
|
||
<th className="py-2 text-left">收盤價差</th>
|
||
<th className="py-2 text-left">結果</th>
|
||
<th className="py-2 text-left">P/L</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{records.map((record) => (
|
||
<tr key={record.recommendation_id} className="border-b border-[#f2e4cb]">
|
||
<td className="py-2 text-[#5f4330]">{record.match_id}</td>
|
||
<td className="py-2 text-[#5f4330]">{record.market_type}</td>
|
||
<td className="py-2 text-[#5f4330]">{record.selection}</td>
|
||
<td className="py-2 text-[#5f4330]">{record.stake}</td>
|
||
<td className="py-2 text-[#5f4330]">{record.recommended_odds}</td>
|
||
<td className="py-2 text-[#5f4330]">{record.closing_odds}</td>
|
||
<td className="py-2 text-[#5f4330]">
|
||
{record.clv_percent === null ? '-' : `${record.clv_percent.toFixed(2)}%`}
|
||
</td>
|
||
<td className="py-2 text-[#5f4330]">{record.is_win ? '命中' : '未中'}</td>
|
||
<td className={`py-2 ${record.pnl >= 0 ? 'text-[#1a9a57]' : 'text-[#8c2f2f]'}`}>
|
||
{record.pnl >= 0 ? '+' : ''}{record.pnl.toFixed(2)}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
{records.length === 0 ? (
|
||
<tr>
|
||
<td className="py-3 text-[#7a5b46]" colSpan={9}>
|
||
目前尚未有可公開核對的建議紀錄
|
||
</td>
|
||
</tr>
|
||
) : null}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</article>
|
||
</section>
|
||
);
|
||
}
|