feat: implement UI design system unification
- Add design-system tokens (colors, spacing, typography, radii, shadows) - Add cn utility for class merging - Refactor StatCard to dark theme (fixes light theme issue) - Refactor PositionTable to use Tailwind CSS (fixes inline styles and color inconsistency) - Remove duplicate AgentTable from ui/ - Update AgentTable in agents/ to use AgentStatus type - Update AgentStatus type to include all status values - Update mockData to match AgentStatus type Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
b5d8d4e71b
131
frontend/src/app/page.tsx
Normal file
131
frontend/src/app/page.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { DollarSign, TrendingUp, Activity, Bot } from "lucide-react";
|
||||||
|
|
||||||
|
import { MetricCard } from "@/components/ui/MetricCard";
|
||||||
|
import { AgentTable } from "@/components/agents/AgentTable";
|
||||||
|
import { TradeList } from "@/components/ui/TradeList";
|
||||||
|
import { EquityChart } from "@/components/ui/EquityChart";
|
||||||
|
import { PnlDistributionChart } from "@/components/ui/PnlDistributionChart";
|
||||||
|
import { getMetrics, getEquityCurve, getPnlDistribution } from "@/api/metrics";
|
||||||
|
import { getAgents } from "@/api/agents";
|
||||||
|
import { getRecentTrades } from "@/api/trades";
|
||||||
|
import type {
|
||||||
|
AgentStatus,
|
||||||
|
TradeRecord,
|
||||||
|
Metrics,
|
||||||
|
EquityPoint,
|
||||||
|
PnlDistribution,
|
||||||
|
} from "@/types/models";
|
||||||
|
|
||||||
|
export default function Dashboard() {
|
||||||
|
const [metrics, setMetrics] = useState<Metrics | null>(null);
|
||||||
|
const [equityData, setEquityData] = useState<EquityPoint[]>([]);
|
||||||
|
const [pnlDistribution, setPnlDistribution] = useState<PnlDistribution[]>([]);
|
||||||
|
const [agents, setAgents] = useState<AgentStatus[]>([]);
|
||||||
|
const [trades, setTrades] = useState<TradeRecord[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchData() {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const [metricsRes, equityRes, pnlRes, agentsRes, tradesRes] =
|
||||||
|
await Promise.all([
|
||||||
|
getMetrics(),
|
||||||
|
getEquityCurve(),
|
||||||
|
getPnlDistribution(),
|
||||||
|
getAgents(),
|
||||||
|
getRecentTrades(10),
|
||||||
|
]);
|
||||||
|
setMetrics(metricsRes);
|
||||||
|
setEquityData(equityRes);
|
||||||
|
setPnlDistribution(pnlRes);
|
||||||
|
setAgents(agentsRes);
|
||||||
|
setTrades(tradesRes);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to fetch data");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="text-[#a0a0a0] animate-pulse">
|
||||||
|
Loading dashboard data...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="text-[#ff5252] font-bold">Error: {error}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Metric Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<MetricCard
|
||||||
|
title="Total Equity"
|
||||||
|
value={metrics?.totalEquity?.toFixed(2) ?? "0.00"}
|
||||||
|
change={metrics?.dailyPnlPercent}
|
||||||
|
changeType={
|
||||||
|
metrics && metrics.dailyPnl >= 0 ? "positive" : "negative"
|
||||||
|
}
|
||||||
|
icon={DollarSign}
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
title="Daily P&L"
|
||||||
|
value={metrics?.dailyPnl?.toFixed(2) ?? "0.00"}
|
||||||
|
change={metrics?.dailyPnlPercent}
|
||||||
|
changeType={
|
||||||
|
metrics && metrics.dailyPnl >= 0 ? "positive" : "negative"
|
||||||
|
}
|
||||||
|
icon={TrendingUp}
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
title="Total Trades"
|
||||||
|
value={metrics?.totalTrades ?? 0}
|
||||||
|
icon={Activity}
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
title="Active Agents"
|
||||||
|
value={`${metrics?.activeAgents ?? 0} / ${metrics?.totalAgents ?? 0}`}
|
||||||
|
icon={Bot}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Charts */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
<div className="lg:col-span-2 bg-[#1e1e1e] border border-[#2c2c2c] rounded-xl overflow-hidden">
|
||||||
|
<EquityChart data={equityData} />
|
||||||
|
</div>
|
||||||
|
<div className="bg-[#1e1e1e] border border-[#2c2c2c] rounded-xl overflow-hidden">
|
||||||
|
<PnlDistributionChart data={pnlDistribution} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tables */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<div className="bg-[#1e1e1e] border border-[#2c2c2c] rounded-xl overflow-hidden">
|
||||||
|
<AgentTable agents={agents} />
|
||||||
|
</div>
|
||||||
|
<div className="bg-[#1e1e1e] border border-[#2c2c2c] rounded-xl overflow-hidden">
|
||||||
|
<TradeList trades={trades} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
131
frontend/src/components/agents/AgentTable.tsx
Normal file
131
frontend/src/components/agents/AgentTable.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Play, Pause, Square, Bot } from "lucide-react";
|
||||||
|
import { cn } from "@/design-system/utils/cn";
|
||||||
|
import type { AgentStatus } from "@/types/models";
|
||||||
|
|
||||||
|
interface AgentTableProps {
|
||||||
|
agents: AgentStatus[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusConfig: Record<
|
||||||
|
string,
|
||||||
|
{ color: string; icon: typeof Play; label: string; textColor: string }
|
||||||
|
> = {
|
||||||
|
running: { color: "bg-[#00ffcc]", icon: Play, label: "RUNNING", textColor: "text-[#00ffcc]" },
|
||||||
|
active: { color: "bg-[#00ffcc]", icon: Play, label: "ACTIVE", textColor: "text-[#00ffcc]" },
|
||||||
|
paused: { color: "bg-[#ffb74d]", icon: Pause, label: "PAUSED", textColor: "text-[#ffb74d]" },
|
||||||
|
resting: { color: "bg-[#ffb74d]", icon: Pause, label: "RESTING", textColor: "text-[#ffb74d]" },
|
||||||
|
stopped: { color: "bg-[#666666]", icon: Square, label: "STOPPED", textColor: "text-[#666666]" },
|
||||||
|
error: { color: "bg-[#ff5252]", icon: Square, label: "ERROR", textColor: "text-[#ff5252]" },
|
||||||
|
danger: { color: "bg-[#ff5252]", icon: Square, label: "DANGER", textColor: "text-[#ff5252]" },
|
||||||
|
bankrupt: { color: "bg-[#ff5252]", icon: Square, label: "BANKRUPT", textColor: "text-[#ff5252]" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AgentTable({ agents }: AgentTableProps) {
|
||||||
|
return (
|
||||||
|
<div className="bg-[#1e1e1e] rounded-2xl border border-[#2c2c2c] overflow-hidden shadow-2xl">
|
||||||
|
<div className="px-6 py-4 border-b border-[#2c2c2c] bg-[#121212] flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-black text-[#e1e1e1] uppercase tracking-widest flex items-center gap-2">
|
||||||
|
<Bot className="w-4 h-4 text-[#00ffcc]" />
|
||||||
|
Deployment Matrix
|
||||||
|
</h3>
|
||||||
|
<span className="text-[10px] font-black text-[#666666] uppercase tracking-widest">
|
||||||
|
{agents.length} Active Nodes
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-[#121212]/50">
|
||||||
|
<th className="px-6 py-3 text-left text-[10px] font-black text-[#666666] uppercase tracking-widest">
|
||||||
|
Unit
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-[10px] font-black text-[#666666] uppercase tracking-widest">
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-[10px] font-black text-[#666666] uppercase tracking-widest">
|
||||||
|
Logic
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-[10px] font-black text-[#666666] uppercase tracking-widest">
|
||||||
|
Asset
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-right text-[10px] font-black text-[#666666] uppercase tracking-widest">
|
||||||
|
P&L
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-right text-[10px] font-black text-[#666666] uppercase tracking-widest">
|
||||||
|
OPS
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-[#2c2c2c]">
|
||||||
|
{agents.map((agent) => {
|
||||||
|
const status = statusConfig[agent.status] || {
|
||||||
|
color: "bg-[#666666]",
|
||||||
|
icon: Square,
|
||||||
|
label: agent.status.toUpperCase(),
|
||||||
|
textColor: "text-[#666666]"
|
||||||
|
};
|
||||||
|
const StatusIcon = status.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={agent.id} className="hover:bg-[#121212] group transition-colors cursor-default">
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-black text-[#e1e1e1] group-hover:text-[#00ffcc] transition-colors">
|
||||||
|
{agent.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] text-[#666666] font-mono uppercase">{agent.id ? agent.id.slice(0, 12) : 'Unknown ID'}</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={cn("w-2 h-2 rounded-full shadow-[0_0_5px_currentColor]", status.color)}
|
||||||
|
/>
|
||||||
|
<span className={cn("text-[10px] font-black uppercase tracking-widest", status.textColor)}>
|
||||||
|
{status.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span className="text-xs font-bold text-[#a0a0a0] uppercase">
|
||||||
|
{agent.strategy}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-black bg-[#00ffcc]/10 text-[#00ffcc] uppercase tracking-tighter">
|
||||||
|
{agent.symbol}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-right">
|
||||||
|
<div>
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
"text-sm font-black italic tracking-tighter",
|
||||||
|
agent.profitLoss >= 0
|
||||||
|
? "text-[#00ffcc]"
|
||||||
|
: "text-[#ff5252]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{agent.profitLoss >= 0 ? "+" : ""}
|
||||||
|
{agent.profitLoss?.toFixed(2) ?? "0.00"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-right">
|
||||||
|
<span className="text-xs font-bold text-[#e1e1e1]">
|
||||||
|
{agent.tradesCount}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AgentTable;
|
||||||
71
frontend/src/components/agents/mockData.ts
Normal file
71
frontend/src/components/agents/mockData.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import type { AgentStatus } from "@/types/models";
|
||||||
|
|
||||||
|
// 虚拟数据
|
||||||
|
export const mockAgents: AgentStatus[] = [
|
||||||
|
{
|
||||||
|
id: "AGT-001",
|
||||||
|
name: "Alpha Trader",
|
||||||
|
status: "running",
|
||||||
|
strategy: "Momentum",
|
||||||
|
symbol: "BTC/USDT",
|
||||||
|
profitLoss: 2580.5,
|
||||||
|
profitLossPercent: 25.8,
|
||||||
|
tradesCount: 156,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "AGT-002",
|
||||||
|
name: "ETH Scalper",
|
||||||
|
status: "active",
|
||||||
|
strategy: "Scalping",
|
||||||
|
symbol: "ETH/USDT",
|
||||||
|
profitLoss: 920.25,
|
||||||
|
profitLossPercent: 9.2,
|
||||||
|
tradesCount: 89,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "AGT-003",
|
||||||
|
name: "Range Bot",
|
||||||
|
status: "resting",
|
||||||
|
strategy: "Mean Reversion",
|
||||||
|
symbol: "SOL/USDT",
|
||||||
|
profitLoss: -360.0,
|
||||||
|
profitLossPercent: -3.6,
|
||||||
|
tradesCount: 45,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "AGT-004",
|
||||||
|
name: "Danger Bot",
|
||||||
|
status: "danger",
|
||||||
|
strategy: "Trend Following",
|
||||||
|
symbol: "DOGE/USDT",
|
||||||
|
profitLoss: -899.25,
|
||||||
|
profitLossPercent: -9.0,
|
||||||
|
tradesCount: 23,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "AGT-005",
|
||||||
|
name: "Failed Bot",
|
||||||
|
status: "bankrupt",
|
||||||
|
strategy: "Arbitrage",
|
||||||
|
symbol: "BNB/USDT",
|
||||||
|
profitLoss: -1000.0,
|
||||||
|
profitLossPercent: -10.0,
|
||||||
|
tradesCount: 12,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "AGT-006",
|
||||||
|
name: "Star Trader",
|
||||||
|
status: "running",
|
||||||
|
strategy: "Grid Trading",
|
||||||
|
symbol: "XRP/USDT",
|
||||||
|
profitLoss: 4670.8,
|
||||||
|
profitLossPercent: 46.7,
|
||||||
|
tradesCount: 312,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
];
|
||||||
144
frontend/src/components/trading/PositionTable.tsx
Normal file
144
frontend/src/components/trading/PositionTable.tsx
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import type { Position } from '@/types/models';
|
||||||
|
import { cn } from '@/design-system/utils/cn';
|
||||||
|
|
||||||
|
interface PositionTableProps {
|
||||||
|
positions: Position[];
|
||||||
|
onClosePosition?: (positionId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PositionTable({ positions, onClosePosition }: PositionTableProps) {
|
||||||
|
return (
|
||||||
|
<div className="bg-[#121212] rounded-lg border border-[#2c2c2c] overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="px-4 py-3 border-b border-[#2c2c2c]">
|
||||||
|
<h3 className="text-sm font-semibold text-[#e1e1e1]">Positions</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-[#1e1e1e]">
|
||||||
|
<th className="px-4 py-2 text-left text-xs font-bold text-[#a0a0a0] uppercase tracking-widest">
|
||||||
|
Symbol
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-2 text-left text-xs font-bold text-[#a0a0a0] uppercase tracking-widest">
|
||||||
|
Side
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-2 text-right text-xs font-bold text-[#a0a0a0] uppercase tracking-widest">
|
||||||
|
Quantity
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-2 text-right text-xs font-bold text-[#a0a0a0] uppercase tracking-widest">
|
||||||
|
Entry Price
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-2 text-right text-xs font-bold text-[#a0a0a0] uppercase tracking-widest">
|
||||||
|
Mark Price
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-2 text-right text-xs font-bold text-[#a0a0a0] uppercase tracking-widest">
|
||||||
|
Unrealized P&L
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-2 text-center text-xs font-bold text-[#a0a0a0] uppercase tracking-widest">
|
||||||
|
Action
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{positions.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={7}
|
||||||
|
className="px-4 py-8 text-center text-sm text-[#666666]"
|
||||||
|
>
|
||||||
|
No positions
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
positions.map((position) => (
|
||||||
|
<tr
|
||||||
|
key={position.id}
|
||||||
|
className="border-b border-[#2c2c2c] hover:bg-[#1e1e1e] transition-colors"
|
||||||
|
>
|
||||||
|
{/* Symbol */}
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap">
|
||||||
|
<span className="text-sm font-medium text-[#e1e1e1]">
|
||||||
|
{position.symbol}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Side */}
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center px-2 py-0.5 rounded text-xs font-medium',
|
||||||
|
position.side === 'long'
|
||||||
|
? 'bg-[rgba(0,255,204,0.15)] text-[#00ffcc]'
|
||||||
|
: 'bg-[rgba(255,82,82,0.15)] text-[#ff5252]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{position.side === 'long' ? 'Long' : 'Short'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Quantity */}
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap text-right">
|
||||||
|
<span className="text-sm text-[#a0a0a0]">{position.quantity}</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Entry Price */}
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap text-right">
|
||||||
|
<span className="text-sm text-[#a0a0a0]">
|
||||||
|
{position.entryPrice.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Mark Price */}
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap text-right">
|
||||||
|
<span className="text-sm text-[#a0a0a0]">
|
||||||
|
{position.markPrice.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Unrealized P&L */}
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap text-right">
|
||||||
|
<div className="flex flex-col items-end">
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
'text-sm font-medium',
|
||||||
|
position.unrealizedPnl >= 0 ? 'text-[#00ffcc]' : 'text-[#ff5252]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{position.unrealizedPnl >= 0 ? '+' : ''}
|
||||||
|
{position.unrealizedPnl.toFixed(2)}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
'text-xs',
|
||||||
|
position.unrealizedPnlPercent >= 0 ? 'text-[#00ffcc]' : 'text-[#ff5252]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{position.unrealizedPnlPercent >= 0 ? '+' : ''}
|
||||||
|
{position.unrealizedPnlPercent.toFixed(2)}%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Action */}
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap text-center">
|
||||||
|
<button
|
||||||
|
onClick={() => onClosePosition?.(position.id)}
|
||||||
|
className="px-3 py-1 text-xs font-medium rounded border border-[#ff5252] bg-transparent text-[#ff5252] hover:bg-[rgba(255,82,82,0.1)] transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
68
frontend/src/components/ui/StatCard.tsx
Normal file
68
frontend/src/components/ui/StatCard.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { cn } from "@/design-system/utils/cn";
|
||||||
|
|
||||||
|
interface StatCardProps {
|
||||||
|
label: string;
|
||||||
|
value: string | number;
|
||||||
|
subValue?: string;
|
||||||
|
change?: {
|
||||||
|
value: number;
|
||||||
|
isPositive: boolean;
|
||||||
|
};
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
isLoading?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatCard({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
subValue,
|
||||||
|
change,
|
||||||
|
icon,
|
||||||
|
isLoading,
|
||||||
|
className,
|
||||||
|
}: StatCardProps) {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("bg-[#1e1e1e] rounded-lg p-6 animate-pulse", className)}
|
||||||
|
>
|
||||||
|
<div className="h-4 w-20 bg-[#2c2c2c] rounded mb-4" />
|
||||||
|
<div className="h-8 w-32 bg-[#2c2c2c] rounded" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"bg-[#1e1e1e] rounded-lg p-6 border border-transparent hover:border-[#2c2c2c] transition-colors",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<span className="text-xs font-bold text-[#a0a0a0] uppercase tracking-widest">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
{icon && <span className="text-[#666666]">{icon}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-baseline gap-3">
|
||||||
|
<span className="text-2xl font-bold text-[#e1e1e1]">{value}</span>
|
||||||
|
{change && (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-sm font-medium",
|
||||||
|
change.isPositive ? "text-[#00ffcc]" : "text-[#ff5252]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{change.isPositive ? "+" : ""}
|
||||||
|
{change.value}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{subValue && <p className="text-sm text-[#a0a0a0] mt-2">{subValue}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
44
frontend/src/design-system/tokens/colors.ts
Normal file
44
frontend/src/design-system/tokens/colors.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
export const colors = {
|
||||||
|
// 背景层级 - 从深到浅
|
||||||
|
background: {
|
||||||
|
base: '#121212', // 页面最底层背景
|
||||||
|
elevated: '#1e1e1e', // 卡片、面板、弹窗
|
||||||
|
overlay: '#2c2c2c', // 悬停状态、下拉菜单
|
||||||
|
sunken: '#0a0a0a', // 输入框、内嵌区域
|
||||||
|
},
|
||||||
|
|
||||||
|
// 品牌色
|
||||||
|
primary: {
|
||||||
|
DEFAULT: '#00ffcc',
|
||||||
|
hover: '#00e6b8',
|
||||||
|
active: '#00cca3',
|
||||||
|
muted: 'rgba(0, 255, 204, 0.1)',
|
||||||
|
glow: 'rgba(0, 255, 204, 0.3)',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 功能色
|
||||||
|
semantic: {
|
||||||
|
success: '#00ffcc', // 与主色保持一致
|
||||||
|
warning: '#ffb74d',
|
||||||
|
error: '#ff5252',
|
||||||
|
info: '#64b5f6',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 文字色
|
||||||
|
text: {
|
||||||
|
primary: '#e1e1e1', // 主要文字
|
||||||
|
secondary: '#a0a0a0', // 次要文字
|
||||||
|
muted: '#666666', // 弱化文字
|
||||||
|
disabled: '#444444', // 禁用状态
|
||||||
|
inverse: '#121212', // 用于主色背景上的文字
|
||||||
|
},
|
||||||
|
|
||||||
|
// 边框
|
||||||
|
border: {
|
||||||
|
DEFAULT: '#2c2c2c',
|
||||||
|
light: '#383838',
|
||||||
|
focus: '#00ffcc',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type Colors = typeof colors;
|
||||||
5
frontend/src/design-system/tokens/index.ts
Normal file
5
frontend/src/design-system/tokens/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export { colors } from './colors';
|
||||||
|
export { spacing } from './spacing';
|
||||||
|
export { typography } from './typography';
|
||||||
|
export { radii } from './radii';
|
||||||
|
export { shadows } from './shadows';
|
||||||
12
frontend/src/design-system/tokens/radii.ts
Normal file
12
frontend/src/design-system/tokens/radii.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export const radii = {
|
||||||
|
none: '0',
|
||||||
|
sm: '0.25rem', // 4px
|
||||||
|
DEFAULT: '0.375rem', // 6px
|
||||||
|
md: '0.5rem', // 8px
|
||||||
|
lg: '0.75rem', // 12px
|
||||||
|
xl: '1rem', // 16px
|
||||||
|
'2xl': '1.5rem', // 24px
|
||||||
|
full: '9999px',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type Radii = typeof radii;
|
||||||
12
frontend/src/design-system/tokens/shadows.ts
Normal file
12
frontend/src/design-system/tokens/shadows.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export const shadows = {
|
||||||
|
none: 'none',
|
||||||
|
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.3)',
|
||||||
|
DEFAULT: '0 4px 6px -1px rgba(0, 0, 0, 0.4)',
|
||||||
|
md: '0 6px 12px -2px rgba(0, 0, 0, 0.5)',
|
||||||
|
lg: '0 10px 20px -4px rgba(0, 0, 0, 0.5)',
|
||||||
|
xl: '0 20px 40px -8px rgba(0, 0, 0, 0.6)',
|
||||||
|
glow: '0 0 20px rgba(0, 255, 204, 0.3)',
|
||||||
|
'glow-sm': '0 0 10px rgba(0, 255, 204, 0.2)',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type Shadows = typeof shadows;
|
||||||
15
frontend/src/design-system/tokens/spacing.ts
Normal file
15
frontend/src/design-system/tokens/spacing.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export const spacing = {
|
||||||
|
0: '0',
|
||||||
|
1: '0.25rem', // 4px
|
||||||
|
2: '0.5rem', // 8px
|
||||||
|
3: '0.75rem', // 12px
|
||||||
|
4: '1rem', // 16px
|
||||||
|
5: '1.25rem', // 20px
|
||||||
|
6: '1.5rem', // 24px
|
||||||
|
8: '2rem', // 32px
|
||||||
|
10: '2.5rem', // 40px
|
||||||
|
12: '3rem', // 48px
|
||||||
|
16: '4rem', // 64px
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type Spacing = typeof spacing;
|
||||||
26
frontend/src/design-system/tokens/typography.ts
Normal file
26
frontend/src/design-system/tokens/typography.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
export const typography = {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
|
||||||
|
mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
|
||||||
|
},
|
||||||
|
|
||||||
|
fontSize: {
|
||||||
|
xs: ['0.75rem', { lineHeight: '1rem', letterSpacing: '0.05em' }],
|
||||||
|
sm: ['0.875rem', { lineHeight: '1.25rem', letterSpacing: '0.025em' }],
|
||||||
|
base: ['1rem', { lineHeight: '1.5rem' }],
|
||||||
|
lg: ['1.125rem', { lineHeight: '1.75rem' }],
|
||||||
|
xl: ['1.25rem', { lineHeight: '1.75rem' }],
|
||||||
|
'2xl': ['1.5rem', { lineHeight: '2rem' }],
|
||||||
|
'3xl': ['1.875rem', { lineHeight: '2.25rem' }],
|
||||||
|
},
|
||||||
|
|
||||||
|
fontWeight: {
|
||||||
|
normal: '400',
|
||||||
|
medium: '500',
|
||||||
|
semibold: '600',
|
||||||
|
bold: '700',
|
||||||
|
black: '900',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type Typography = typeof typography;
|
||||||
6
frontend/src/design-system/utils/cn.ts
Normal file
6
frontend/src/design-system/utils/cn.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from 'clsx';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
1
frontend/src/design-system/utils/index.ts
Normal file
1
frontend/src/design-system/utils/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { cn } from './cn';
|
||||||
176
frontend/src/types/models.ts
Normal file
176
frontend/src/types/models.ts
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
export interface AgentStatus {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
status:
|
||||||
|
| "running"
|
||||||
|
| "active"
|
||||||
|
| "paused"
|
||||||
|
| "resting"
|
||||||
|
| "stopped"
|
||||||
|
| "error"
|
||||||
|
| "danger"
|
||||||
|
| "bankrupt";
|
||||||
|
strategy: string;
|
||||||
|
symbol: string;
|
||||||
|
profitLoss: number;
|
||||||
|
profitLossPercent: number;
|
||||||
|
tradesCount: number;
|
||||||
|
lastUpdated: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TradeRecord {
|
||||||
|
id: string;
|
||||||
|
agentId: string;
|
||||||
|
agentName: string;
|
||||||
|
symbol: string;
|
||||||
|
side: "buy" | "sell";
|
||||||
|
quantity: number;
|
||||||
|
price: number;
|
||||||
|
total: number;
|
||||||
|
timestamp: string;
|
||||||
|
pnl?: number;
|
||||||
|
pnlPercent?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Metrics {
|
||||||
|
totalEquity: number;
|
||||||
|
dailyPnl: number;
|
||||||
|
dailyPnlPercent: number;
|
||||||
|
totalTrades: number;
|
||||||
|
winRate: number;
|
||||||
|
activeAgents: number;
|
||||||
|
totalAgents: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EquityPoint {
|
||||||
|
timestamp: string;
|
||||||
|
equity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PnlDistribution {
|
||||||
|
range: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trading related types
|
||||||
|
export interface Order {
|
||||||
|
id: string;
|
||||||
|
symbol: string;
|
||||||
|
side: "buy" | "sell";
|
||||||
|
type: "market" | "limit" | "stop" | "stop_limit";
|
||||||
|
status:
|
||||||
|
| "pending"
|
||||||
|
| "open"
|
||||||
|
| "filled"
|
||||||
|
| "partially_filled"
|
||||||
|
| "cancelled"
|
||||||
|
| "rejected";
|
||||||
|
quantity: number;
|
||||||
|
filledQuantity: number;
|
||||||
|
price?: number;
|
||||||
|
stopPrice?: number;
|
||||||
|
total?: number;
|
||||||
|
timestamp: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Position {
|
||||||
|
id: string;
|
||||||
|
symbol: string;
|
||||||
|
side: "long" | "short";
|
||||||
|
quantity: number;
|
||||||
|
entryPrice: number;
|
||||||
|
markPrice: number;
|
||||||
|
liquidationPrice?: number;
|
||||||
|
margin: number;
|
||||||
|
leverage: number;
|
||||||
|
unrealizedPnl: number;
|
||||||
|
unrealizedPnlPercent: number;
|
||||||
|
realizedPnl: number;
|
||||||
|
openedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Ticker {
|
||||||
|
symbol: string;
|
||||||
|
lastPrice: number;
|
||||||
|
priceChange: number;
|
||||||
|
priceChangePercent: number;
|
||||||
|
high24h: number;
|
||||||
|
low24h: number;
|
||||||
|
volume24h: number;
|
||||||
|
quoteVolume24h: number;
|
||||||
|
bidPrice: number;
|
||||||
|
askPrice: number;
|
||||||
|
bidQty: number;
|
||||||
|
askQty: number;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderBookEntry {
|
||||||
|
price: number;
|
||||||
|
quantity: number;
|
||||||
|
total?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderBook {
|
||||||
|
symbol: string;
|
||||||
|
bids: OrderBookEntry[];
|
||||||
|
asks: OrderBookEntry[];
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Kline {
|
||||||
|
timestamp: string;
|
||||||
|
open: number;
|
||||||
|
high: number;
|
||||||
|
low: number;
|
||||||
|
close: number;
|
||||||
|
volume: number;
|
||||||
|
quoteVolume: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TradingMode = "simulation" | "live";
|
||||||
|
|
||||||
|
// Trade type for recent trades
|
||||||
|
export interface Trade {
|
||||||
|
id: string;
|
||||||
|
symbol: string;
|
||||||
|
side: "buy" | "sell";
|
||||||
|
price: number;
|
||||||
|
quantity: number;
|
||||||
|
total: number;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exchange configuration
|
||||||
|
export interface Exchange {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: "binance" | "okx" | "bybit" | "custom";
|
||||||
|
apiKey: string;
|
||||||
|
apiSecret?: string;
|
||||||
|
passphrase?: string;
|
||||||
|
testnet: boolean;
|
||||||
|
enabled?: boolean;
|
||||||
|
status: "connected" | "disconnected" | "error";
|
||||||
|
lastTested?: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExchangeCreateRequest {
|
||||||
|
name: string;
|
||||||
|
type: "binance" | "okx" | "bybit" | "custom";
|
||||||
|
apiKey: string;
|
||||||
|
apiSecret: string;
|
||||||
|
passphrase?: string;
|
||||||
|
testnet: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExchangeUpdateRequest {
|
||||||
|
name?: string;
|
||||||
|
apiKey?: string;
|
||||||
|
apiSecret?: string;
|
||||||
|
passphrase?: string;
|
||||||
|
testnet?: boolean;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user