const { useState, useEffect, useCallback } = React;
const API = "/api";
let ADMIN_TOKEN = (typeof window !== "undefined" && window.localStorage)
? (window.localStorage.getItem("ASTER_ADMIN_TOKEN") || "")
: "";
function ensureAdminToken() {
if (!ADMIN_TOKEN && typeof window !== "undefined") {
const t = window.prompt("请输入 Dashboard Admin Token(一次保存)", "");
if (t && t.trim()) {
ADMIN_TOKEN = t.trim();
try { window.localStorage.setItem("ASTER_ADMIN_TOKEN", ADMIN_TOKEN); } catch {}
}
}
return ADMIN_TOKEN;
}
// ─── Palette: dark terminal aesthetic ───
const C = {
bg: "#0a0e17",
card: "#111827",
cardHover: "#1a2236",
border: "#1e293b",
borderActive: "#334155",
text: "#e2e8f0",
textDim: "#64748b",
textMuted: "#475569",
accent: "#22d3ee",
accentDim: "#0891b2",
green: "#10b981",
greenDim: "#065f46",
red: "#ef4444",
redDim: "#7f1d1d",
yellow: "#f59e0b",
yellowDim: "#78350f",
purple: "#a78bfa",
};
// ─── Utility ───
const fmt = (n, d = 2) => (n !== null && n !== undefined ? Number(n).toFixed(d) : "—");
const fmtUsd = (n) => (n !== null && n !== undefined ? `$${Number(n).toFixed(2)}` : "—");
async function api(path, opts = {}) {
try {
const token = ensureAdminToken();
const authHeaders = token ? { Authorization: `Bearer ${token}` } : {};
const res = await fetch(`${API}${path}`, {
headers: { "Content-Type": "application/json", ...authHeaders, ...(opts.headers || {}) },
...opts,
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
if (res.status === 401) {
ADMIN_TOKEN = "";
try { window.localStorage.removeItem("ASTER_ADMIN_TOKEN"); } catch {}
}
throw new Error(err.detail || `HTTP ${res.status}`);
}
return await res.json();
} catch (e) {
console.error(`API Error [${path}]:`, e);
throw e;
}
}
// ─── Components ───
function StatusDot({ active, color }) {
const c = color || (active ? C.green : C.textMuted);
return (
);
}
function Badge({ children, color = C.accent }) {
return (
{children}
);
}
function Card({ title, children, action, style = {} }) {
return (
{(title || action) && (
{title && (
{title}
)}
{action}
)}
{children}
);
}
function Btn({ children, onClick, color = C.accent, small, disabled, style = {} }) {
const [hover, setHover] = useState(false);
return (
);
}
function Input({ label, value, onChange, placeholder, type = "text", style = {} }) {
return (
{label && }
onChange(e.target.value)}
placeholder={placeholder}
style={{
width: "100%",
background: C.bg,
border: `1px solid ${C.border}`,
borderRadius: 6,
padding: "8px 12px",
color: C.text,
fontSize: 13,
outline: "none",
boxSizing: "border-box",
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
}}
/>
);
}
function Select({ label, value, onChange, options, style = {} }) {
return (
{label && }
);
}
// ─── Setup Panel ───
function SetupPanel({ onConnected }) {
const [apiKey, setApiKey] = useState("");
const [apiSecret, setApiSecret] = useState("");
const [baseUrl, setBaseUrl] = useState("https://fapi.asterdex.com");
const [chain, setChain] = useState("bnb");
const [status, setStatus] = useState(null);
const [loading, setLoading] = useState(false);
const connect = async () => {
setLoading(true);
try {
const res = await api("/account/configure", {
method: "POST",
body: JSON.stringify({ api_key: apiKey, api_secret: apiSecret, base_url: baseUrl, chain }),
});
setStatus(res);
if (res.status === "connected") {
setTimeout(() => onConnected(), 500);
}
} catch (e) {
setStatus({ status: "error", message: e.message });
}
setLoading(false);
};
return (
);
}
// ─── Stats Row ───
function StatsRow({ stats, positions }) {
const totalUnrealizedPnl = positions.reduce((sum, p) => sum + (p.unrealized_pnl || 0), 0);
const items = [
{ label: "Total PnL", value: fmtUsd(stats.total_pnl), color: (stats.total_pnl || 0) >= 0 ? C.green : C.red },
{ label: "Unrealized PnL", value: fmtUsd(totalUnrealizedPnl), color: totalUnrealizedPnl >= 0 ? C.green : C.red },
{ label: "Open Positions", value: positions.length, color: C.accent },
{ label: "Total Orders", value: stats.total_orders || 0, color: C.text },
{ label: "Total Trades", value: stats.total_trades || 0, color: C.text },
{ label: "Active Alerts", value: stats.active_alerts || 0, color: (stats.active_alerts || 0) > 0 ? C.yellow : C.textDim },
];
return (
{items.map((item) => (
{item.label}
{item.value}
))}
);
}
// ─── Positions Table ───
function PositionsTable({ positions }) {
if (!positions.length) {
return (
No open positions
);
}
return (
{["Symbol", "Side", "Size", "Entry", "Mark", "PnL", "Leverage"].map((h) => (
| {h} |
))}
{positions.map((p, i) => (
| {p.symbol} |
0 ? C.green : C.red}>{p.quantity > 0 ? "LONG" : "SHORT"} |
{fmt(Math.abs(p.quantity), 4)} |
{fmt(p.entry_price)} |
{fmt(p.mark_price)} |
= 0 ? C.green : C.red, fontWeight: 600 }}>
{fmtUsd(p.unrealized_pnl)}
|
{p.leverage}x |
))}
);
}
// ─── Strategy Panel ───
function StrategyPanel({ strategies, onRefresh }) {
const [showCreate, setShowCreate] = useState(false);
const [types, setTypes] = useState([]);
const [form, setForm] = useState({ type: "twap", name: "", symbol: "BTCUSDT", params: "{}" });
const [twap, setTwap] = useState({
total_quantity: "0.01",
num_slices: "5",
duration_minutes: "10",
side: "BUY",
order_type: "MARKET",
});
const [iceberg, setIceberg] = useState({
side: "BUY",
total_quantity: "0.01",
min_qty: "0.001",
max_qty: "0.002",
min_interval: "8",
max_interval: "25",
price_offset_ticks: "0",
max_offset_ticks: "2",
duration_minutes: "30",
pause_on_spread_bps: "30",
max_deviation_bps: "80",
buy_ceiling_price: "",
sell_floor_price: "",
leverage: "3",
margin_type: "ISOLATED",
use_post_only: true,
use_binance_anchor: false,
binance_symbol: "UAIUSDT",
binance_offset_pct: "-0.3",
});
useEffect(() => {
api("/strategies/types").then((d) => setTypes(d.types || [])).catch(() => {});
}, []);
const twapPreview = (() => {
const tq = parseFloat(twap.total_quantity || "0") || 0;
const ns = Math.max(1, parseInt(twap.num_slices || "1", 10));
const dm = parseFloat(twap.duration_minutes || "0") || 0;
const per = ns > 0 ? tq / ns : 0;
const every = ns > 0 ? (dm / ns) : 0;
return "将" + twap.side + " " + tq + "(" + form.symbol + "),共 " + ns + " 笔,每笔约 " + per.toFixed(6) + ",约每 " + every.toFixed(2) + " 分钟一笔";
})();
const icebergPreview = (() => {
const tq = parseFloat(iceberg.total_quantity || "0") || 0;
const q1 = parseFloat(iceberg.min_qty || "0") || 0;
const q2 = parseFloat(iceberg.max_qty || "0") || 0;
const i1 = parseFloat(iceberg.min_interval || "0") || 0;
const i2 = parseFloat(iceberg.max_interval || "0") || 0;
const band = iceberg.side === "BUY" ? (iceberg.buy_ceiling_price || "未设") : (iceberg.sell_floor_price || "未设");
return "将" + iceberg.side + " " + tq + "(" + form.symbol + "),每次随机挂 " + q1 + "~" + q2 + ",间隔 " + i1 + "~" + i2 + " 秒,杠杆 " + iceberg.leverage + "x,价格边界=" + band;
})();
const buildParams = () => {
if (form.type === "twap") {
return {
total_quantity: parseFloat(twap.total_quantity || "0"),
num_slices: parseInt(twap.num_slices || "1", 10),
duration_minutes: parseFloat(twap.duration_minutes || "0"),
side: twap.side,
order_type: twap.order_type,
};
}
if (form.type === "iceberg") {
return {
side: iceberg.side,
total_quantity: parseFloat(iceberg.total_quantity || "0"),
min_qty: parseFloat(iceberg.min_qty || "0"),
max_qty: parseFloat(iceberg.max_qty || "0"),
min_interval: parseFloat(iceberg.min_interval || "0"),
max_interval: parseFloat(iceberg.max_interval || "0"),
price_offset_ticks: parseInt(iceberg.price_offset_ticks || "0", 10),
max_offset_ticks: parseInt(iceberg.max_offset_ticks || "0", 10),
duration_minutes: parseFloat(iceberg.duration_minutes || "0"),
pause_on_spread_bps: parseFloat(iceberg.pause_on_spread_bps || "30"),
max_deviation_bps: parseFloat(iceberg.max_deviation_bps || "80"),
buy_ceiling_price: iceberg.buy_ceiling_price ? parseFloat(iceberg.buy_ceiling_price) : 0,
sell_floor_price: iceberg.sell_floor_price ? parseFloat(iceberg.sell_floor_price) : 0,
leverage: parseInt(iceberg.leverage || "3", 10),
margin_type: iceberg.margin_type,
use_post_only: !!iceberg.use_post_only,
use_binance_anchor: !!iceberg.use_binance_anchor,
binance_symbol: iceberg.binance_symbol || form.symbol,
binance_offset_pct: parseFloat(iceberg.binance_offset_pct || "0"),
};
}
let params = {};
try { params = JSON.parse(form.params || "{}"); } catch {}
return params;
};
const createStrategy = async () => {
try {
const params = buildParams();
await api("/strategies", {
method: "POST",
body: JSON.stringify({ strategy_type: form.type, name: form.name, symbol: form.symbol, params }),
});
setShowCreate(false);
setForm({ type: "twap", name: "", symbol: "BTCUSDT", params: "{}" });
onRefresh();
} catch (e) {
alert(e.message);
}
};
const applyTwapPreset = (preset) => {
if (preset === "safe") setTwap({ total_quantity: "0.005", num_slices: "10", duration_minutes: "30", side: "BUY", order_type: "MARKET" });
else if (preset === "normal") setTwap({ total_quantity: "0.01", num_slices: "5", duration_minutes: "10", side: "BUY", order_type: "MARKET" });
else if (preset === "fast") setTwap({ total_quantity: "0.01", num_slices: "3", duration_minutes: "3", side: "BUY", order_type: "MARKET" });
};
const applyIcebergPreset = (preset) => {
if (preset === "safe") setIceberg({ side: "BUY", total_quantity: "0.005", min_qty: "0.0005", max_qty: "0.001", min_interval: "12", max_interval: "30", price_offset_ticks: "0", max_offset_ticks: "2", duration_minutes: "45", pause_on_spread_bps: "25", max_deviation_bps: "60", buy_ceiling_price: "", sell_floor_price: "", leverage: "2", margin_type: "ISOLATED", use_post_only: true });
else if (preset === "normal") setIceberg({ side: "BUY", total_quantity: "0.01", min_qty: "0.001", max_qty: "0.002", min_interval: "8", max_interval: "25", price_offset_ticks: "0", max_offset_ticks: "2", duration_minutes: "30", pause_on_spread_bps: "30", max_deviation_bps: "80", buy_ceiling_price: "", sell_floor_price: "", leverage: "3", margin_type: "ISOLATED", use_post_only: true });
else if (preset === "fast") setIceberg({ side: "BUY", total_quantity: "0.01", min_qty: "0.0015", max_qty: "0.003", min_interval: "3", max_interval: "10", price_offset_ticks: "0", max_offset_ticks: "1", duration_minutes: "10", pause_on_spread_bps: "40", max_deviation_bps: "120", buy_ceiling_price: "", sell_floor_price: "", leverage: "5", margin_type: "ISOLATED", use_post_only: true });
else if (preset === "uai_short") setIceberg({ side: "SELL", total_quantity: "8000", min_qty: "120", max_qty: "360", min_interval: "6", max_interval: "18", price_offset_ticks: "0", max_offset_ticks: "2", duration_minutes: "90", pause_on_spread_bps: "12", max_deviation_bps: "35", buy_ceiling_price: "", sell_floor_price: "0.1976", leverage: "3", margin_type: "ISOLATED", use_post_only: true, use_binance_anchor: false, binance_symbol: "UAIUSDT", binance_offset_pct: "-0.3" });
else if (preset === "uai_bn_short") setIceberg({ side: "SELL", total_quantity: "8000", min_qty: "120", max_qty: "360", min_interval: "6", max_interval: "18", price_offset_ticks: "0", max_offset_ticks: "2", duration_minutes: "90", pause_on_spread_bps: "12", max_deviation_bps: "35", buy_ceiling_price: "", sell_floor_price: "0.1970", leverage: "3", margin_type: "ISOLATED", use_post_only: true, use_binance_anchor: true, binance_symbol: "UAIUSDT", binance_offset_pct: "-0.3" });
};
const toggleStrategy = async (sid, state) => {
try {
if (state === "running") await api("/strategies/" + sid + "/stop", { method: "POST" });
else await api("/strategies/" + sid + "/start", { method: "POST" });
onRefresh();
} catch (e) { alert(e.message); }
};
const deleteStrategy = async (sid) => {
try { await api("/strategies/" + sid, { method: "DELETE" }); onRefresh(); }
catch (e) { alert(e.message); }
};
return (
setShowCreate(!showCreate)}>{showCreate ? "Cancel" : "+ New"}}>
{showCreate && (
{form.type === "twap" ? (
) : form.type === "iceberg" ? (
Iceberg 简单模式(隐藏式 maker 执行)
applyIcebergPreset("safe")}>保守模板
applyIcebergPreset("normal")}>标准模板
applyIcebergPreset("fast")}>快速模板
applyIcebergPreset("uai_short")}>UAI做空3x
applyIcebergPreset("uai_bn_short")}>UAI-BN(-0.3%)空3x
{icebergPreview}
) : (
setForm({ ...form, params: v })} placeholder='{"quantity": 0.001}' style={{ marginTop: 10 }} />
)}
Create Strategy
)}
{strategies.length === 0 ? (
No strategies created yet. Click "+ New" to create one.
) : (
strategies.map((s) => (
{s.name}
{s.symbol}
{s.strategy_id}
{Object.entries(s.params || {}).slice(0, 4).map(([k, v]) => k + "=" + v).join(" · ")}
toggleStrategy(s.strategy_id, s.state)} color={s.state === "running" ? C.red : C.green}>{s.state === "running" ? "Stop" : "Start"}
deleteStrategy(s.strategy_id)} color={C.textDim}>✕
))
)}
);
}
// ─── Orders Table ───
function OrdersTable({ orders }) {
return (
{orders.length === 0 ? (
No orders yet
) : (
{["Time", "Strategy", "Symbol", "Side", "Type", "Qty", "Price", "Status"].map((h) => (
| {h} |
))}
{orders.slice(0, 50).map((o, i) => (
| {o.created_at?.slice(5, 19)} |
{o.strategy_id?.slice(0, 6) || "manual"} |
{o.symbol} |
{o.side} |
{o.order_type} |
{fmt(o.quantity, 4)} |
{fmt(o.price)} |
{o.status} |
))}
)}
);
}
// ─── Alerts Panel ───
function AlertsPanel({ alerts }) {
const levelColor = { CRITICAL: C.red, WARNING: C.yellow, INFO: C.accent };
return (
{alerts.length === 0 ? (
No alerts — system healthy
) : (
{alerts.map((a, i) => (
{a.level}
{a.message}
{a.category} · {a.timestamp?.slice(0, 19)}
))}
)}
);
}
// ─── Risk Config Panel ───
function RiskPanel({ riskStatus }) {
if (!riskStatus) return null;
const config = riskStatus.config || {};
const items = [
{ label: "Max Order Value", value: fmtUsd(config.max_order_value_usd) },
{ label: "Max Daily Loss", value: fmtUsd(config.max_daily_loss_usd) },
{ label: "Max Drawdown", value: `${config.max_drawdown_pct}%` },
{ label: "Max Orders/min", value: config.max_orders_per_minute },
{ label: "Daily PnL", value: fmtUsd(riskStatus.daily_pnl), color: (riskStatus.daily_pnl || 0) >= 0 ? C.green : C.red },
{ label: "Enabled", value: config.enabled ? "YES" : "NO", color: config.enabled ? C.green : C.red },
];
return (
{items.map((item) => (
{item.label}
{item.value}
))}
);
}
// ─── Manual Trade Panel ───
function ManualTradePanel() {
const [symbol, setSymbol] = useState("BTCUSDT");
const [side, setSide] = useState("BUY");
const [qty, setQty] = useState("");
const [price, setPrice] = useState("");
const [orderType, setOrderType] = useState("MARKET");
const [result, setResult] = useState(null);
const placeTrade = async () => {
try {
const body = { symbol, side, order_type: orderType, quantity: parseFloat(qty) };
if (orderType === "LIMIT" && price) body.price = parseFloat(price);
if (orderType === "LIMIT") body.time_in_force = "GTC";
const res = await api("/trading/order", { method: "POST", body: JSON.stringify(body) });
setResult({ ok: true, msg: `Order placed: ${res.order_id} ${res.status}` });
} catch (e) {
setResult({ ok: false, msg: e.message });
}
};
return (
{side} {symbol}
{result && (
{result.msg}
)}
);
}
// ─── Main Dashboard ───
function Dashboard() {
const [connected, setConnected] = useState(null); // null = checking, true, false
const [strategies, setStrategies] = useState([]);
const [positions, setPositions] = useState([]);
const [orders, setOrders] = useState([]);
const [alerts, setAlerts] = useState([]);
const [stats, setStats] = useState({});
const [riskStatus, setRiskStatus] = useState(null);
const [tab, setTab] = useState("overview");
const [error, setError] = useState(null);
const checkHealth = useCallback(async () => {
try {
const h = await api("/health");
setConnected(h.connected);
setError(null);
} catch {
setError("Cannot reach trading server. Make sure it's running on port 8888.");
setConnected(false);
}
}, []);
const fetchAll = useCallback(async () => {
try {
const [strats, pos, ords, alrts, sts, risk] = await Promise.allSettled([
api("/strategies"),
api("/account/positions"),
api("/data/orders?limit=50"),
api("/risk/alerts?limit=20"),
api("/data/stats"),
api("/risk/status"),
]);
if (strats.status === "fulfilled") setStrategies(strats.value || []);
if (pos.status === "fulfilled") setPositions(pos.value || []);
if (ords.status === "fulfilled") setOrders(ords.value || []);
if (alrts.status === "fulfilled") setAlerts(alrts.value || []);
if (sts.status === "fulfilled") setStats(sts.value || {});
if (risk.status === "fulfilled") setRiskStatus(risk.value);
} catch {}
}, []);
useEffect(() => {
checkHealth();
const interval = setInterval(checkHealth, 10000);
return () => clearInterval(interval);
}, [checkHealth]);
useEffect(() => {
if (connected) {
fetchAll();
const interval = setInterval(fetchAll, 5000);
return () => clearInterval(interval);
}
}, [connected, fetchAll]);
// Server unreachable
if (error) {
return (
⚠
Server Unreachable
{error}
cd aster-trading && python main.py
);
}
// Not connected to Aster
if (connected === false) {
return (
{ setConnected(true); fetchAll(); }} />
);
}
// Loading
if (connected === null) {
return (
);
}
const tabs = [
{ id: "overview", label: "Overview" },
{ id: "strategies", label: "Strategies" },
{ id: "trade", label: "Manual Trade" },
{ id: "risk", label: "Risk" },
];
return (
{/* Header */}
⬡ ASTER
Trading System
{tabs.map((t) => (
))}
Connected
{/* Content */}
{tab === "overview" && (
)}
{tab === "strategies" && (
)}
{tab === "trade" && (
)}
{tab === "risk" && (
)}
);
}
window.Dashboard = Dashboard;