175 lines
9.3 KiB
TypeScript
175 lines
9.3 KiB
TypeScript
export const dynamic = 'force-dynamic';
|
||
|
||
import { Card, CardContent } from "@/components/ui/card";
|
||
import { Badge } from "@/components/ui/badge";
|
||
|
||
async function getTrades() {
|
||
const DB = "/home/fzq1228/polyscout/data/polyscout.db";
|
||
const { execSync } = await import("child_process");
|
||
const py = `import sqlite3,json; conn=sqlite3.connect('${DB}'); c=conn.cursor(); c.execute("""SELECT market_id, question, bet_direction, entry_price, bet_amount, end_date, status, pnl, created_at FROM paper_trades ORDER BY created_at DESC LIMIT 50"""); print(json.dumps(c.fetchall(), ensure_ascii=False, default=str)); conn.close()`;
|
||
try { return JSON.parse(execSync(`python3 -c ${JSON.stringify(py)}`, { encoding: "utf8" })); }
|
||
catch { return []; }
|
||
}
|
||
|
||
async function getStats() {
|
||
const DB = "/home/fzq1228/polyscout/data/polyscout.db";
|
||
const { execSync } = await import("child_process");
|
||
const py = `import sqlite3,json; conn=sqlite3.connect('${DB}'); c=conn.cursor(); c.execute("SELECT status, COUNT(*) FROM paper_trades GROUP BY status"); print(json.dumps(c.fetchall(), ensure_ascii=False)); conn.close()`;
|
||
try { return JSON.parse(execSync(`python3 -c ${JSON.stringify(py)}`, { encoding: "utf8" })); }
|
||
catch { return []; }
|
||
}
|
||
|
||
function calcExpectedProfit(direction: string, entryPrice: number, betAmount: number): number {
|
||
const price = direction === "YES" ? entryPrice : 1 - entryPrice;
|
||
return betAmount * (1 / price - 1);
|
||
}
|
||
|
||
function renderCountdown(endDateStr: unknown) {
|
||
const originalDate = String(endDateStr).slice(0, 10);
|
||
const endDate = new Date(String(endDateStr));
|
||
if (isNaN(endDate.getTime())) return <span className="text-muted-foreground">{originalDate}</span>;
|
||
const now = new Date();
|
||
const diffMs = endDate.getTime() - now.getTime();
|
||
const diffDays = diffMs / (1000 * 60 * 60 * 24);
|
||
|
||
if (diffMs <= 0) {
|
||
return <span className="text-muted-foreground" title={originalDate}>已过期</span>;
|
||
} else if (diffDays > 7) {
|
||
return <span className="text-orange-400" title={originalDate}>{Math.floor(diffDays)}天后</span>;
|
||
} else if (diffDays >= 3) {
|
||
return <span className="text-yellow-400" title={originalDate}>{Math.floor(diffDays)}天后</span>;
|
||
} else {
|
||
const days = Math.floor(diffDays);
|
||
const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||
return <span className="text-red-400 font-bold animate-pulse" title={originalDate}>{days}天 {hours}h后</span>;
|
||
}
|
||
}
|
||
|
||
const statusMap: Record<string, string> = { open: "进行中", won: "盈利 ✅", lost: "亏损 ❌" };
|
||
const statusVariant: Record<string, "default" | "secondary" | "destructive"> = {
|
||
open: "secondary", won: "default", lost: "destructive"
|
||
};
|
||
|
||
export default async function TradesPage() {
|
||
const [rows, stats] = await Promise.all([getTrades(), getStats()]);
|
||
const statsMap = Object.fromEntries(stats.map((s: any[]) => [s[0], s[1]]));
|
||
const totalBetAmount = rows.reduce((sum: number, r: any[]) => sum + (Number(r[4]) || 0), 0);
|
||
const totalPnl = rows.reduce((sum: number, r: any[]) => sum + (r[7] || 0), 0);
|
||
const expectedTotalProfit = rows
|
||
.filter((r: any[]) => r[6] === "open")
|
||
.reduce((sum: number, r: any[]) => sum + calcExpectedProfit(r[2], Number(r[3]), Number(r[4])), 0);
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<div>
|
||
<h1 className="text-2xl font-bold">模拟交易</h1>
|
||
<p className="text-muted-foreground text-sm mt-1">信号验证 — 模拟下注记录(每注 $1)</p>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 md:grid-cols-6 gap-3">
|
||
<Card>
|
||
<CardContent className="pt-4 pb-4">
|
||
<div className="text-xs text-muted-foreground">总下注金额</div>
|
||
<div className="text-2xl font-bold mt-1">${totalBetAmount.toFixed(2)}</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardContent className="pt-4 pb-4">
|
||
<div className="text-xs text-muted-foreground">总注单</div>
|
||
<div className="text-2xl font-bold mt-1">{rows.length}</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardContent className="pt-4 pb-4">
|
||
<div className="text-xs text-muted-foreground">进行中</div>
|
||
<div className="text-2xl font-bold mt-1">{statsMap["open"] || 0}</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardContent className="pt-4 pb-4">
|
||
<div className="text-xs text-muted-foreground">已盈利</div>
|
||
<div className="text-2xl font-bold mt-1 text-green-400">{statsMap["won"] || 0}</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardContent className="pt-4 pb-4">
|
||
<div className="text-xs text-muted-foreground">模拟总盈亏</div>
|
||
<div className={`text-2xl font-bold mt-1 ${totalPnl >= 0 ? "text-green-400" : "text-red-400"}`}>
|
||
{totalPnl >= 0 ? "+" : ""}{totalPnl.toFixed(2)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardContent className="pt-4 pb-4">
|
||
<div className="text-xs text-muted-foreground">预期总收益</div>
|
||
<div className={`text-2xl font-bold mt-1 ${expectedTotalProfit >= 0 ? "text-blue-400" : "text-red-400"}`}>
|
||
{expectedTotalProfit >= 0 ? "+$" : "-$"}{Math.abs(expectedTotalProfit).toFixed(2)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
<Card>
|
||
<CardContent className="p-0">
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-xs">
|
||
<thead>
|
||
<tr className="border-b border-border bg-muted/30">
|
||
<th className="text-left px-3 py-2 text-muted-foreground font-medium uppercase tracking-wider">市场问题</th>
|
||
<th className="text-left px-3 py-2 text-muted-foreground font-medium uppercase tracking-wider">方向</th>
|
||
<th className="text-left px-3 py-2 text-muted-foreground font-medium uppercase tracking-wider">入场价</th>
|
||
<th className="text-left px-3 py-2 text-muted-foreground font-medium uppercase tracking-wider hidden sm:table-cell">下注额</th>
|
||
<th className="text-left px-3 py-2 text-muted-foreground font-medium uppercase tracking-wider hidden sm:table-cell">预期收益</th>
|
||
<th className="text-left px-3 py-2 text-muted-foreground font-medium uppercase tracking-wider">到期日</th>
|
||
<th className="text-left px-3 py-2 text-muted-foreground font-medium uppercase tracking-wider">状态</th>
|
||
<th className="text-left px-3 py-2 text-muted-foreground font-medium uppercase tracking-wider">盈亏</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{rows.length === 0 ? (
|
||
<tr><td colSpan={8} className="text-center py-8 text-muted-foreground">暂无模拟交易</td></tr>
|
||
) : rows.map((r: any[], i: number) => {
|
||
const expProfit = calcExpectedProfit(r[2], Number(r[3]), Number(r[4]));
|
||
return (
|
||
<tr key={i} className="border-b border-border last:border-0 hover:bg-accent/50 transition-colors">
|
||
<td className="px-3 py-2 max-w-[160px] sm:max-w-xs md:max-w-sm">
|
||
<div className="truncate font-medium leading-tight" title={r[1]}>{r[1]}</div>
|
||
<div className="font-mono text-[10px] text-muted-foreground mt-0.5">{String(r[0]).slice(0, 8)}</div>
|
||
</td>
|
||
<td className="px-3 py-2 whitespace-nowrap">
|
||
<span className={`font-bold px-1.5 py-0.5 rounded ${r[2] === "YES" ? "bg-green-500/20 text-green-400" : "bg-red-500/20 text-red-400"}`}>
|
||
{r[2]}
|
||
</span>
|
||
</td>
|
||
<td className="px-3 py-2 font-mono whitespace-nowrap">{(Number(r[3]) * 100).toFixed(1)}¢</td>
|
||
<td className="px-3 py-2 font-mono whitespace-nowrap hidden sm:table-cell">${Number(r[4]).toFixed(2)}</td>
|
||
<td className="px-3 py-2 font-mono whitespace-nowrap hidden sm:table-cell">
|
||
{r[6] === "open" ? (
|
||
<span className="text-blue-400">+${expProfit.toFixed(2)}</span>
|
||
) : <span className="text-muted-foreground">-</span>}
|
||
</td>
|
||
<td className="px-3 py-2 font-mono whitespace-nowrap">{renderCountdown(r[5])}</td>
|
||
<td className="px-3 py-2 whitespace-nowrap">
|
||
<Badge variant={statusVariant[r[6]] ?? "secondary"} className="text-[10px] px-1.5 py-0">
|
||
{statusMap[r[6]] ?? r[6]}
|
||
</Badge>
|
||
</td>
|
||
<td className="px-3 py-2 font-mono whitespace-nowrap">
|
||
{r[7] != null ? (
|
||
<span className={r[7] >= 0 ? "text-green-400" : "text-red-400"}>
|
||
{r[7] >= 0 ? "+" : ""}{Number(r[7]).toFixed(2)}
|
||
</span>
|
||
) : <span className="text-muted-foreground">-</span>}
|
||
</td>
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|