// Data, hooks, icons — shared across all pages.
const { useState, useEffect, useRef, useMemo, useCallback } = React;
// ============================================================
// ICONS — Lucide-style, hand-drawn paths
// ============================================================
const Icon = ({ d, size = 18, stroke = 1.6, fill = "none", style }) => (
);
const IconHome = (p) => >} />;
const IconSignal = (p) => >} />;
const IconChart = (p) => >} />;
const IconShield = (p) => >} />;
const IconClock = (p) => >} />;
const IconBell = (p) => >} />;
const IconList = (p) => >} />;
const IconTerm = (p) => >} />;
const IconGear = (p) => >} />;
const IconSearch = (p) => >} />;
const IconPlus = (p) => >} />;
const IconPause = (p) => >} />;
const IconPlay = (p) => >} />;
const IconEdit = (p) => >} />;
const IconStop = (p) => >} />;
const IconChevron = (p) => >} />;
const IconUpgrade = (p) => >} />;
const IconCheck = (p) => >} />;
const IconX = (p) => >} />;
const IconCopy = (p) => >} />;
const IconEye = (p) => >} />;
const IconEyeOff = (p) => >} />;
const IconExternal= (p) => >} />;
const IconFilter = (p) => >} />;
const IconRefresh = (p) => >} />;
const IconDown = (p) => >} />;
const IconAlert = (p) => >} />;
const IconKey = (p) => >} />;
const IconBolt = (p) => >} />;
const IconRocket = (p) => >} />;
const IconSwap = (p) => >} />;
const IconLayers = (p) => >} />;
const IconLogo = ({ size = 22 }) => (
);
// Asset glyphs
const IconBinance = ({ size = 18 }) => (
);
const IconTV = ({ size = 18 }) => (
);
const IconTelegram = ({ size = 18 }) => (
);
// Kraken — stylised tentacle/spiral mark in brand purple
const IconKraken = ({ size = 18 }) => (
);
// ============================================================
// EXCHANGES — config per supported venue (spec v3)
// ============================================================
const EXCHANGES = {
binance: {
id: "binance",
name: "Binance",
full: "Binance.com (international)",
Icon: IconBinance,
color: "#F0B90B",
ccxtId: "binance",
nativeSymbol: "BTCUSDT",
fees: { maker: 0.10, taker: 0.10 },
rateLimit: "1200 weight/min · 10 orders/sec",
oco: true,
stopLoss: "OCO order",
stopLossNote: "exchange-side · survives bot crashes",
testnet: true,
testnetUrl: "testnet.binance.vision",
docs: "binance-docs.github.io/apidocs/spot",
rttMs: 47,
quirks: "Native BTCUSDT symbol · CCXT normalises to BTC/USDT",
},
kraken: {
id: "kraken",
name: "Kraken",
full: "Kraken Spot",
Icon: IconKraken,
color: "#5741D9",
ccxtId: "kraken",
nativeSymbol: "XXBTZUSD",
fees: { maker: 0.16, taker: 0.26 },
rateLimit: "15 calls / 3s · counter decays 1/3s",
oco: false,
stopLoss: "Conditional close",
stopLossNote: "attached to opening order via close param",
testnet: false,
testnetUrl: "no public testnet — paper-trade mode",
docs: "docs.kraken.com/api",
rttMs: 88,
quirks: "Uses XBT internally · CCXT maps to BTC · stricter rate limit",
},
};
// ============================================================
// LIVE-TICKER + PRNG + HOOKS
// ============================================================
function useTicker(initial, { volatility = 0.0008, intervalMs = 1400, bias = 0 } = {}) {
const [value, setValue] = useState(initial);
const [dir, setDir] = useState(0);
useEffect(() => {
let stop = false;
const tick = () => {
if (stop) return;
setValue((v) => {
const drift = (Math.random() - 0.5 + bias) * volatility * v;
const next = Math.max(0.01, v + drift);
setDir(drift > 0 ? 1 : drift < 0 ? -1 : 0);
return next;
});
};
const id = setInterval(tick, intervalMs + Math.random() * 600);
return () => { stop = true; clearInterval(id); };
}, [volatility, intervalMs, bias]);
return [value, dir];
}
function seedRand(seed) {
let s = seed;
return () => { s = (s * 9301 + 49297) % 233280; return s / 233280; };
}
function generateSeries(n, { seed = 1, start = 100, drift = 0.0008, vol = 0.012 } = {}) {
const rand = seedRand(seed);
const out = [start];
for (let i = 1; i < n; i++) {
const d = (rand() - 0.5) * vol + drift;
out.push(Math.max(0.01, out[i - 1] * (1 + d)));
}
return out;
}
function useCountUp(target, duration = 900) {
const [val, setVal] = useState(target);
const fromRef = useRef(target);
useEffect(() => {
const from = fromRef.current;
const start = performance.now();
let raf;
const step = (t) => {
const p = Math.min(1, (t - start) / duration);
const eased = 1 - Math.pow(1 - p, 3);
setVal(from + (target - from) * eased);
if (p < 1) raf = requestAnimationFrame(step);
else fromRef.current = target;
};
raf = requestAnimationFrame(step);
return () => cancelAnimationFrame(raf);
}, [target, duration]);
return val;
}
// ============================================================
// SCENARIOS (Tweaks-driven)
// ============================================================
const SCENARIOS = {
winning: { tone: "profit", equity: 11842.30, change24h: 132.18, changePct: 1.13, todayTrades: 7, todayWin: 6, dailyLoss: 0.42, drawdown: 2.3 },
losing: { tone: "loss", equity: 9680.12, change24h: -204.50, changePct: -2.07, todayTrades: 9, todayWin: 3, dailyLoss: 2.71, drawdown: 8.4 },
volatile: { tone: "warn", equity: 10520.74, change24h: 18.24, changePct: 0.17, todayTrades: 14, todayWin: 7, dailyLoss: 1.18, drawdown: 5.6 },
};
// ============================================================
// BOT — the MVP is a single bot. Source of truth.
// ============================================================
const BOT = {
name: "BTC/USDT EMA Cross",
pair: "BTC/USDT",
activeExchange: "binance", // spec v3: single-active via ACTIVE_EXCHANGE
market: "Spot",
timeframe: "5m",
strategy: "EMA(9/21) Cross + RSI(14) > 50",
source: "TradingView Pine v5",
mode: "testnet", // testnet | live | paper
status: "Running",
uptimeSec: 184302, // ~2d 3h
region: "Dubai · Hetzner",
startBalance: 10000,
};
// ============================================================
// SIGNALS — TradingView webhook log
// ============================================================
const SIGNALS = [
{ t: -12, id: "sig-0a7c", side: "BUY", price: 64210.50, status: "executed", reason: null, latencyMs: 412 },
{ t: -184, id: "sig-0a7b", side: "SELL", price: 64055.40, status: "executed", reason: null, latencyMs: 538 },
{ t: -640, id: "sig-0a7a", side: "BUY", price: 63980.10, status: "executed", reason: null, latencyMs: 689 },
{ t: -1020, id: "sig-0a79", side: "BUY", price: 63920.00, status: "deduplicated", reason: "Duplicate signal_id (< 60s)", latencyMs: 24 },
{ t: -1810, id: "sig-0a78", side: "SELL", price: 63810.20, status: "executed", reason: null, latencyMs: 461 },
{ t: -2440, id: "sig-0a77", side: "BUY", price: 63702.00, status: "rejected", reason: "Position already open", latencyMs: 18 },
{ t: -3110, id: "sig-0a76", side: "SELL", price: 63812.50, status: "stopped_out", reason: "Stop loss hit @ 1.98%", latencyMs: 488 },
{ t: -4290, id: "sig-0a75", side: "BUY", price: 64104.00, status: "executed", reason: null, latencyMs: 612 },
{ t: -5640, id: "sig-0a74", side: "BUY", price: 64012.10, status: "rejected", reason: "Min notional $10 not met", latencyMs: 22 },
{ t: -7100, id: "sig-0a73", side: "SELL", price: 64198.40, status: "executed", reason: null, latencyMs: 384 },
{ t: -9200, id: "sig-0a72", side: "BUY", price: 63972.00, status: "executed", reason: null, latencyMs: 502 },
{ t: -12400, id: "sig-0a71", side: "BUY", price: 63810.00, status: "rejected", reason: "Daily loss limit reached", latencyMs: 14 },
];
// ============================================================
// TRADES — full history
// ============================================================
const TRADES = [
{ t: -12, exchange: "binance", side: "BUY", qty: 0.0084, entry: 64210.50, exit: null, pnl: null, pnlPct: null, stop: 62926.30, closedBy: null, status: "open" },
{ t: -184, exchange: "binance", side: "SELL", qty: 0.0082, entry: 63702.00, exit: 64055.40, pnl: 28.97, pnlPct: 0.55, stop: null, closedBy: "signal", status: "closed" },
{ t: -640, exchange: "binance", side: "BUY", qty: 0.0085, entry: 63702.00, exit: 63980.10, pnl: 23.64, pnlPct: 0.44, stop: null, closedBy: "signal", status: "closed" },
{ t: -1810, exchange: "binance", side: "SELL", qty: 0.0083, entry: 64104.00, exit: 63810.20, pnl: 24.39, pnlPct: 0.46, stop: null, closedBy: "signal", status: "closed" },
{ t: -3110, exchange: "binance", side: "SELL", qty: 0.0080, entry: 65085.30, exit: 63812.50, pnl: -101.82,pnlPct: -1.96, stop: 63812.50, closedBy: "stop_loss",status: "stopped_out" },
{ t: -4290, exchange: "binance", side: "BUY", qty: 0.0086, entry: 64104.00, exit: 65085.30, pnl: 84.39, pnlPct: 1.53, stop: null, closedBy: "signal", status: "closed" },
{ t: -7100, exchange: "kraken", side: "SELL", qty: 0.0082, entry: 63972.00, exit: 64198.40, pnl: 18.56, pnlPct: 0.35, stop: null, closedBy: "signal", status: "closed" },
{ t: -9200, exchange: "kraken", side: "BUY", qty: 0.0084, entry: 63972.00, exit: 64198.40, pnl: 19.02, pnlPct: 0.35, stop: null, closedBy: "signal", status: "closed" },
{ t: -14800, exchange: "binance", side: "BUY", qty: 0.0080, entry: 63540.00, exit: 64104.00, pnl: 45.12, pnlPct: 0.89, stop: null, closedBy: "signal", status: "closed" },
{ t: -22400, exchange: "binance", side: "SELL", qty: 0.0079, entry: 63820.00, exit: 63540.00, pnl: 22.12, pnlPct: 0.44, stop: null, closedBy: "signal", status: "closed" },
{ t: -29600, exchange: "kraken", side: "BUY", qty: 0.0083, entry: 63820.00, exit: 64210.00, pnl: 32.37, pnlPct: 0.61, stop: null, closedBy: "signal", status: "closed" },
{ t: -38800, exchange: "kraken", side: "SELL", qty: 0.0082, entry: 64198.00, exit: 64822.00, pnl: -51.17, pnlPct: -0.97, stop: 64822.00, closedBy: "stop_loss",status: "stopped_out" },
{ t: -52400, exchange: "binance", side: "BUY", qty: 0.0080, entry: 64198.00, exit: 65010.00, pnl: 64.96, pnlPct: 1.26, stop: null, closedBy: "signal", status: "closed" },
{ t: -68000, exchange: "binance", side: "SELL", qty: 0.0078, entry: 64850.00, exit: 64198.00, pnl: 50.86, pnlPct: 1.01, stop: null, closedBy: "signal", status: "closed" },
{ t: -86400, exchange: "binance", side: "BUY", qty: 0.0081, entry: 64124.00, exit: 64850.00, pnl: 58.81, pnlPct: 1.13, stop: null, closedBy: "signal", status: "closed" },
];
// ============================================================
// LOGS — structured JSON entries
// ============================================================
const LOGS = [
{ t: -12, level: "INFO", type: "trade_executed", msg: "Market BUY 0.0084 BTC/USDT @ 64210.50 · OCO stop @ 62926.30", ctx: { signal_id: "sig-0a7c", order_id: "B-4419821", latency_ms: 412 } },
{ t: -45, level: "DEBUG", type: "risk_check_passed", msg: "All 6 pre-trade checks passed", ctx: { signal_id: "sig-0a7c" } },
{ t: -60, level: "INFO", type: "signal_received", msg: "Webhook BUY BTC/USDT @ 64210.50", ctx: { signal_id: "sig-0a7c", source_ip: "52.89.214.238" } },
{ t: -184, level: "INFO", type: "trade_executed", msg: "Market SELL 0.0082 BTC/USDT @ 64055.40 · PnL +$28.97", ctx: { signal_id: "sig-0a7b", order_id: "B-4419742" } },
{ t: -1020, level: "WARN", type: "signal_deduplicated", msg: "Dropped duplicate signal_id within 60s window", ctx: { signal_id: "sig-0a79", age_s: 14 } },
{ t: -2440, level: "WARN", type: "risk_check_failed", msg: "Rejected: position already open (max_concurrent_positions=1)", ctx: { signal_id: "sig-0a77" } },
{ t: -3110, level: "INFO", type: "stop_loss_triggered", msg: "OCO stop filled @ 63812.50 · PnL -$101.82", ctx: { signal_id: "sig-0a76" } },
{ t: -5640, level: "WARN", type: "risk_check_failed", msg: "Position size $4.20 below Binance min notional $10", ctx: { signal_id: "sig-0a74" } },
{ t: -7220, level: "DEBUG", type: "exchange_api_call", msg: "GET /api/v3/account · 47ms", ctx: { endpoint: "/api/v3/account" } },
{ t: -12400, level: "ERROR",type: "risk_check_failed", msg: "Daily loss limit reached: -3.02% (cap -3.00%)", ctx: { signal_id: "sig-0a71" } },
{ t: -12500, level: "ERROR",type: "daily_limit_hit", msg: "Circuit breaker tripped · halting all trading", ctx: {} },
{ t: -14200, level: "INFO", type: "bot_startup", msg: "Bot started · mode=testnet · exchange=binance · balance=$10,000.00", ctx: { mode: "testnet" } },
{ t: -14210, level: "INFO", type: "state_recovery", msg: "Reconciled 1 open position from exchange · 0 orphans", ctx: { open: 1 } },
];
// ============================================================
// Daily P&L for the last 14 days (for bar chart)
// ============================================================
const DAILY_PNL = [
{ d: -13, pnl: 82.40, trades: 6, wr: 0.83 },
{ d: -12, pnl: -34.10, trades: 5, wr: 0.40 },
{ d: -11, pnl: 124.50, trades: 8, wr: 0.75 },
{ d: -10, pnl: 18.20, trades: 4, wr: 0.50 },
{ d: -9, pnl: -88.40, trades: 9, wr: 0.33 },
{ d: -8, pnl: 162.30, trades: 7, wr: 0.86 },
{ d: -7, pnl: 204.18, trades:11, wr: 0.73 },
{ d: -6, pnl: 56.40, trades: 5, wr: 0.60 },
{ d: -5, pnl: -42.10, trades: 6, wr: 0.33 },
{ d: -4, pnl: 128.30, trades: 9, wr: 0.78 },
{ d: -3, pnl: 96.80, trades: 7, wr: 0.71 },
{ d: -2, pnl: -18.40, trades: 4, wr: 0.50 },
{ d: -1, pnl: 184.20, trades: 8, wr: 0.75 },
{ d: 0, pnl: 132.18, trades: 7, wr: 0.86 },
];
// Backtest monthly returns (rows = year, cols = month)
const MONTHLY = [
{ year: 2024, vals: [1.2, -0.8, 3.4, 2.1, 0.9, -1.4, 4.2, 1.8, 2.9, -2.1, 3.6, 4.4] },
{ year: 2025, vals: [2.4, 1.1, -1.8, 3.2, 4.1, 2.7, 1.4, -0.6, 3.8, 2.1, 1.9, 2.8] },
{ year: 2026, vals: [3.1, 2.6, 1.9, 4.2, 2.1, null, null, null, null, null, null, null] },
];
// ============================================================
// BUILD PROGRESS — 37 tasks across 8 categories (spec v3 task_status)
// ============================================================
const TASK_CATEGORIES = [
{ id: "foundation", label: "Foundation", color: "var(--info)" },
{ id: "database", label: "Database", color: "#A78BFA" },
{ id: "risk", label: "Risk", color: "var(--accent)" },
{ id: "execution", label: "Execution", color: "var(--profit)" },
{ id: "api", label: "API", color: "#60A5FA" },
{ id: "alerts", label: "Alerts", color: "#F472B6" },
{ id: "recovery", label: "Recovery", color: "var(--warn)" },
{ id: "deployment", label: "Deployment", color: "var(--text-2)" },
];
const TASKS = [
{ id: "T01", cat: "foundation", task: "Initialize project structure and folder layout", status: "done" },
{ id: "T02", cat: "foundation", task: "Create .env.example with all environment variables", status: "done" },
{ id: "T03", cat: "foundation", task: "Build config.py with Pydantic settings loader", status: "done" },
{ id: "T04", cat: "foundation", task: "Set up requirements.txt with pinned dependencies", status: "done" },
{ id: "T05", cat: "foundation", task: "Create .gitignore", status: "done" },
{ id: "T06", cat: "database", task: "Set up SQLAlchemy async engine with WAL mode", status: "done" },
{ id: "T07", cat: "database", task: "Create Trade model (with exchange column)", status: "done" },
{ id: "T08", cat: "database", task: "Create Signal model", status: "done" },
{ id: "T09", cat: "database", task: "Create DailyStats model", status: "done" },
{ id: "T10", cat: "risk", task: "Build RiskManager with pre-trade validation", status: "done" },
{ id: "T11", cat: "risk", task: "Implement position sizing (fixed fraction)", status: "done" },
{ id: "T12", cat: "risk", task: "Build circuit breaker", status: "done" },
{ id: "T13", cat: "risk", task: "Implement max drawdown kill switch", status: "done" },
{ id: "T14", cat: "risk", task: "Write tests for risk manager", status: "in-progress" },
{ id: "T15", cat: "execution", task: "Build AbstractExchangeClient base class (ABC interface)", status: "done" },
{ id: "T16", cat: "execution", task: "Build ExchangeFactory (reads ACTIVE_EXCHANGE → client)", status: "done" },
{ id: "T17", cat: "execution", task: "Build BinanceClient (OCO orders, testnet support)", status: "done" },
{ id: "T18", cat: "execution", task: "Build KrakenClient (conditional close, paper-trade mode)", status: "in-progress" },
{ id: "T19", cat: "execution", task: "Implement testnet / live / paper mode toggle", status: "in-progress" },
{ id: "T20", cat: "execution", task: "Build order manager (exchange-agnostic dispatch)", status: "done" },
{ id: "T21", cat: "execution", task: "Add retry logic with exponential backoff (3 retries)", status: "in-progress" },
{ id: "T22", cat: "execution", task: "Write tests for exchange factory + both clients (mocked)", status: "pending" },
{ id: "T23", cat: "api", task: "Create FastAPI app with Uvicorn entry point", status: "done" },
{ id: "T24", cat: "api", task: "Build POST /webhook with secret validation", status: "done" },
{ id: "T25", cat: "api", task: "Implement signal deduplication (60s window)", status: "done" },
{ id: "T26", cat: "api", task: "Build GET /health endpoint (includes active_exchange)", status: "in-progress" },
{ id: "T27", cat: "api", task: "Add rate limiting via SlowAPI", status: "done" },
{ id: "T28", cat: "api", task: "Create Pydantic models for webhook payload", status: "done" },
{ id: "T29", cat: "alerts", task: "Build Telegram bot with async sender", status: "done" },
{ id: "T30", cat: "alerts", task: "Create message templates for all events", status: "done" },
{ id: "T31", cat: "alerts", task: "Implement /stop /resume /status /balance /today commands", status: "in-progress" },
{ id: "T32", cat: "alerts", task: "Build daily summary scheduler (23:59 UTC)", status: "pending" },
{ id: "T33", cat: "recovery", task: "Build state reconciler (exchange-aware)", status: "pending" },
{ id: "T34", cat: "deployment", task: "Set up structured JSON logging with rotation", status: "in-progress" },
{ id: "T35", cat: "deployment", task: "Create Dockerfile", status: "pending" },
{ id: "T36", cat: "deployment", task: "Create docker-compose.yml with health checks", status: "pending" },
{ id: "T37", cat: "deployment", task: "Write README.md with setup guide (both exchanges)", status: "pending" },
];
// ============================================================
// ROADMAP — v1 / v2 / v3 (spec v3)
// ============================================================
const ROADMAP = [
{
v: "v1", name: "Reliable Execution", timeline: "Weeks 1–3", current: true,
status: "in-progress",
features: [
"Single pair BTC/USDT",
"TradingView webhook → FastAPI",
"Signal dedup + validation",
"Risk manager with pre-trade checks",
"Binance + Kraken via CCXT (single-active)",
"Exchange factory pattern",
"OCO stop / conditional close",
"Position sizing (fixed fraction)",
"Daily loss circuit breaker",
"Telegram alerts + /commands",
"State recovery on restart",
"Health check endpoint",
"Docker deployment",
"Testnet / paper mode default",
],
},
{
v: "v2", name: "Professional Trading System", timeline: "Months 2–3",
status: "planned",
features: [
"Multi-pair support",
"Futures / perpetual trading",
"Trailing stop loss",
"Take profit (fixed + trailing)",
"Web dashboard (FastAPI + React)",
"PostgreSQL migration",
"Trade history + analytics",
"Multi-exchange (Bybit, OKX)",
"Docker Compose + NGINX + HTTPS",
"Grafana + Prometheus monitoring",
"Auto-restart with systemd",
],
},
{
v: "v3", name: "AI Quant Platform", timeline: "Months 4–6+",
status: "future",
features: [
"Market regime detection (ML)",
"Sentiment analysis (news + social)",
"AI strategy optimization",
"Reinforcement learning signals",
"Adaptive position sizing",
"Portfolio balancing",
"Order book analytics",
"Volatility engine",
"Backtesting cluster",
],
},
];
// Changelog distilled from spec
const CHANGELOG = [
{
date: "2026-05-15 01:30 UTC", agent: "claude_chat", version: "spec v3.0 · MVP V1.1",
summary: "Added Kraken as second supported exchange",
highlights: [
"Exchange factory pattern (AbstractExchangeClient → BinanceClient, KrakenClient)",
"Single-active per deploy via ACTIVE_EXCHANGE env var",
"Kraken specifics: conditional close, paper-trade mode (no public testnet), stricter rate limit",
"Added 3 new tasks (T15–T17 factory split) · 34 → 37 total",
"Added ACTIVE_EXCHANGE, KRAKEN_API_KEY, KRAKEN_API_SECRET, TRADING_PAIR env vars",
"trades table gains `exchange` column",
"Health endpoint returns active_exchange + trading_mode (testnet | live | paper)",
],
},
{
date: "2026-05-15 00:00 UTC", agent: "claude_chat", version: "spec v2.0",
summary: "Project spec finalized · 34-task plan",
highlights: [
"Framework: Flask → FastAPI (async-native)",
"Max daily loss: 5% → 3%",
"Stop loss expanded: boolean → full OCO config",
"Removed Codex API · deferred Bybit/OKX/BingX to v2",
"Added testnet default, signal dedup (60s), state recovery, /health, rate limiting",
"Added 8 clarifying questions for build kickoff",
],
},
];
// Hourly heatmap — wins per hour-of-day (UTC) over 30d
const HOURLY = (() => {
const r = seedRand(7);
return Array.from({ length: 24 }, (_, h) => ({
hour: h,
pnl: (r() - 0.4) * 90 + (h >= 13 && h <= 17 ? 40 : 0), // London/NY overlap edge
trades: Math.floor(r() * 8) + 1,
}));
})();
// Format helpers
function fmtUsd(n, { dec = 2, sign = false } = {}) {
if (n == null) return "—";
const s = Math.abs(n).toLocaleString("en-US", { minimumFractionDigits: dec, maximumFractionDigits: dec });
return (sign ? (n >= 0 ? "+" : "−") : (n < 0 ? "−" : "")) + "$" + s;
}
function fmtPct(n, { dec = 2, sign = true } = {}) {
if (n == null) return "—";
return (sign ? (n >= 0 ? "+" : "") : "") + n.toFixed(dec) + "%";
}
function fmtRel(secAgo) {
const s = -secAgo;
if (s < 60) return s + "s ago";
if (s < 3600) return Math.round(s / 60) + "m ago";
if (s < 86400) return Math.round(s / 3600) + "h ago";
return Math.round(s / 86400) + "d ago";
}
function fmtDuration(sec) {
const d = Math.floor(sec / 86400);
const h = Math.floor((sec % 86400) / 3600);
const m = Math.floor((sec % 3600) / 60);
if (d) return d + "d " + h.toString().padStart(2, "0") + "h " + m.toString().padStart(2, "0") + "m";
if (h) return h + "h " + m.toString().padStart(2, "0") + "m";
return m + "m";
}
function fmtTime(secAgo) {
const dt = new Date(Date.now() + secAgo * 1000);
return dt.toISOString().slice(11, 19); // HH:MM:SS UTC
}
Object.assign(window, {
Icon, IconHome, IconSignal, IconChart, IconShield, IconClock, IconBell, IconList, IconTerm, IconGear,
IconSearch, IconPlus, IconPause, IconPlay, IconEdit, IconStop, IconChevron, IconUpgrade,
IconCheck, IconX, IconCopy, IconEye, IconEyeOff, IconExternal, IconFilter, IconRefresh, IconDown, IconAlert, IconKey, IconBolt,
IconRocket, IconSwap, IconLayers,
IconLogo, IconBinance, IconKraken, IconTV, IconTelegram,
useTicker, useCountUp, generateSeries, seedRand,
EXCHANGES, TASK_CATEGORIES, TASKS, ROADMAP, CHANGELOG,
SCENARIOS, BOT, SIGNALS, TRADES, LOGS, DAILY_PNL, MONTHLY, HOURLY,
fmtUsd, fmtPct, fmtRel, fmtDuration, fmtTime,
});