polyscout-web/app/trades/page.tsx
2026-02-26 10:15:35 +00:00

152 lines
8.3 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.

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);
}
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 text-muted-foreground">{String(r[5]).slice(0, 10)}</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>
);
}