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:
ZhangPeng 2026-02-26 22:36:31 +08:00
commit b5d8d4e71b
14 changed files with 842 additions and 0 deletions

131
frontend/src/app/page.tsx Normal file
View 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>
);
}

View 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;

View 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(),
},
];

View 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>
);
}

View 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>
);
}

View 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;

View 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';

View 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;

View 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;

View 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;

View 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;

View 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));
}

View File

@ -0,0 +1 @@
export { cn } from './cn';

View 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;
}