omnitrade-mcp 0.9.4 → 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  <p align="center">
2
2
  <br />
3
3
  <picture>
4
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Connectry-io/omnitrade-mcp/main/.github/assets/logo-dark.svg">
5
- <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Connectry-io/omnitrade-mcp/main/.github/assets/logo-light.svg">
6
- <img alt="OmniTrade" src="https://raw.githubusercontent.com/Connectry-io/omnitrade-mcp/main/.github/assets/logo-dark.svg" width="350">
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Connectry-io/connectrylab-omnitrade-mcp/main/.github/assets/logo-dark.svg">
5
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Connectry-io/connectrylab-omnitrade-mcp/main/.github/assets/logo-light.svg">
6
+ <img alt="OmniTrade" src="https://raw.githubusercontent.com/Connectry-io/connectrylab-omnitrade-mcp/main/.github/assets/logo-dark.svg" width="350">
7
7
  </picture>
8
8
  <br />
9
9
  </p>
@@ -20,13 +20,14 @@
20
20
  <p align="center">
21
21
  <a href="https://www.npmjs.com/package/omnitrade-mcp"><img src="https://img.shields.io/npm/v/omnitrade-mcp?style=flat&colorA=18181B&colorB=28CF8D" alt="npm version"></a>
22
22
  <a href="https://www.npmjs.com/package/omnitrade-mcp"><img src="https://img.shields.io/npm/dm/omnitrade-mcp?style=flat&colorA=18181B&colorB=28CF8D" alt="npm downloads"></a>
23
- <a href="https://github.com/Connectry-io/omnitrade-mcp"><img src="https://img.shields.io/github/stars/Connectry-io/omnitrade-mcp?style=flat&colorA=18181B&colorB=28CF8D" alt="GitHub stars"></a>
24
- <a href="https://github.com/Connectry-io/omnitrade-mcp/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Connectry-io/omnitrade-mcp?style=flat&colorA=18181B&colorB=28CF8D" alt="License"></a>
23
+ <a href="https://github.com/Connectry-io/connectrylab-omnitrade-mcp"><img src="https://img.shields.io/github/stars/Connectry-io/connectrylab-omnitrade-mcp?style=flat&colorA=18181B&colorB=28CF8D" alt="GitHub stars"></a>
24
+ <a href="https://github.com/Connectry-io/connectrylab-omnitrade-mcp/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Connectry-io/connectrylab-omnitrade-mcp?style=flat&colorA=18181B&colorB=28CF8D" alt="License"></a>
25
25
  </p>
26
26
 
27
27
  <p align="center">
28
28
  <a href="#-quick-start">Quick Start</a> •
29
29
  <a href="#-features">Features</a> •
30
+ <a href="#-desktop-app">Desktop App</a> •
30
31
  <a href="#-examples">Examples</a> •
31
32
  <a href="#-tui-dashboard">Dashboard</a> •
32
33
  <a href="#-paper-trading">Paper Trading</a> •
@@ -36,7 +37,7 @@
36
37
 
37
38
  ---
38
39
 
39
- > ⚠️ **Disclaimer:** OmniTrade is a developer tool and does not constitute financial advice. Cryptocurrency trading involves substantial risk. Connectry Labs is not a licensed financial advisor. Always do your own research and consult a qualified financial advisor before making investment decisions. [Use at your own risk.](https://github.com/Connectry-io/omnitrade-mcp/blob/main/LICENSE)
40
+ > ⚠️ **Disclaimer:** OmniTrade is a developer tool and does not constitute financial advice. Cryptocurrency trading involves substantial risk. Connectry Labs is not a licensed financial advisor. Always do your own research and consult a qualified financial advisor before making investment decisions. [Use at your own risk.](https://github.com/Connectry-io/connectrylab-omnitrade-mcp/blob/main/LICENSE)
40
41
 
41
42
  ## 💬 What is OmniTrade?
42
43
 
@@ -119,6 +120,26 @@ Practice risk-free with a virtual $10,000 USDT wallet. Buy and sell at live mark
119
120
 
120
121
  <br />
121
122
 
123
+ ## 🖥️ Desktop App
124
+
125
+ OmniTrade ships a native desktop GUI built with Tauri — Dashboard, Prices, Portfolio, Alerts, DCA, and Settings in a standalone app.
126
+
127
+ ### Download
128
+
129
+ | Platform | File | Notes |
130
+ |----------|------|-------|
131
+ | macOS | `.dmg` | Built automatically on each release |
132
+ | Windows | `.msi` / `.exe` | Built automatically on each release |
133
+ | Linux (portable) | `.AppImage` | Run without installing |
134
+ | Linux (Debian/Ubuntu) | `.deb` | `dpkg -i OmniTrade_*.deb` |
135
+ | Linux (RedHat/Fedora) | `.rpm` | `rpm -i OmniTrade-*.rpm` |
136
+
137
+ 👉 **[Download from GitHub Releases](https://github.com/Connectry-io/connectrylab-omnitrade-mcp/releases)**
138
+
139
+ The desktop app reads your existing `~/.omnitrade/config.json` — no extra setup needed after running `omnitrade setup`.
140
+
141
+ ---
142
+
122
143
  ## 🚀 Quick Start
123
144
 
124
145
  ### 1. Install
@@ -499,13 +520,17 @@ The wizard walks you through:
499
520
  | Channel | How to configure |
500
521
  |---------|-----------------|
501
522
  | **Native OS** | Zero setup — uses system notifications on macOS, Windows, and Linux |
502
- | **Telegram** | Create a bot via [@BotFather](https://t.me/BotFather), get your token + chat ID |
523
+ | **Telegram** | Create a bot via [@BotFather](https://t.me/BotFather) → get the **Bot Token** (long string like `7481234567:AAHdqTcvCH1v...`), then get your **Chat ID** (numeric, from the getUpdates URL) |
503
524
  | **Discord** | Create a webhook in your server's channel settings, paste the URL |
504
525
 
526
+ > **API key input:** Keys and secrets are masked as you type — nothing shown in plaintext in the terminal.
527
+
505
528
  4. **Claude integration** — auto-writes to Claude Desktop config and optionally `~/.claude/settings.json` for Claude Code
506
529
 
507
530
  You can re-run `omnitrade setup` at any time to update credentials or add new notification channels.
508
531
 
532
+ > **Tip:** Re-running setup won't wipe your existing config. It detects your current keys and lets you keep, update, or skip each exchange individually.
533
+
509
534
  ### Dashboard — Full-Screen TUI *(v0.9.0)*
510
535
 
511
536
  Launch the Bloomberg Terminal-style interface with live prices, charts, and portfolio panel.
@@ -677,7 +702,7 @@ OmniTrade provides **40 tools** organized by category:
677
702
  - ✅ **API keys stay on your machine** — Never sent anywhere else
678
703
  - ✅ **No cloud storage** — Everything local
679
704
  - ✅ **No telemetry** — Zero data collection
680
- - ✅ **Open source** — [Audit the code yourself](https://github.com/Connectry-io/omnitrade-mcp)
705
+ - ✅ **Open source** — [Audit the code yourself](https://github.com/Connectry-io/connectrylab-omnitrade-mcp)
681
706
 
682
707
  ### API Key Best Practices
683
708
 
@@ -741,7 +766,7 @@ We love contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
741
766
 
742
767
  ```bash
743
768
  # Clone the repo
744
- git clone https://github.com/Connectry-io/omnitrade-mcp.git
769
+ git clone https://github.com/Connectry-io/connectrylab-omnitrade-mcp.git
745
770
  cd omnitrade-mcp
746
771
 
747
772
  # Install dependencies
@@ -791,7 +816,7 @@ OmniTrade is a project by [Connectry Labs](https://connectry.io/labs) — the in
791
816
  </p>
792
817
 
793
818
  <p align="center">
794
- <a href="https://github.com/Connectry-io/omnitrade-mcp">GitHub</a> •
819
+ <a href="https://github.com/Connectry-io/connectrylab-omnitrade-mcp">GitHub</a> •
795
820
  <a href="https://www.npmjs.com/package/omnitrade-mcp">npm</a> •
796
821
  <a href="https://connectry.io/labs/omnitrade">Website</a>
797
822
  </p>
package/dist/cli.js CHANGED
@@ -13,7 +13,10 @@ import { homedir } from "os";
13
13
  import { join } from "path";
14
14
  import * as readline from "readline";
15
15
  import { spawn } from "child_process";
16
- var VERSION = "0.9.4";
16
+ import { createRequire } from "module";
17
+ var _require = createRequire(import.meta.url);
18
+ var _pkg = _require("../package.json");
19
+ var VERSION = _pkg.version;
17
20
  var CONFIG_PATH = join(homedir(), ".omnitrade", "config.json");
18
21
  var c = {
19
22
  reset: "\x1B[0m",
@@ -29,6 +32,36 @@ var c = {
29
32
  orange: "\x1B[38;5;208m",
30
33
  red: "\x1B[38;5;196m"
31
34
  };
35
+ async function maskedQuestion(prompt) {
36
+ return new Promise((resolve) => {
37
+ process.stdout.write(prompt);
38
+ let input = "";
39
+ process.stdin.setRawMode(true);
40
+ process.stdin.resume();
41
+ process.stdin.setEncoding("utf8");
42
+ process.stdin.on("data", function handler(char) {
43
+ if (char === "\r" || char === "\n") {
44
+ process.stdin.setRawMode(false);
45
+ process.stdin.pause();
46
+ process.stdin.removeListener("data", handler);
47
+ process.stdout.write("\n");
48
+ resolve(input);
49
+ } else if (char === "") {
50
+ process.exit();
51
+ } else if (char === "\x7F") {
52
+ if (input.length > 0) {
53
+ input = input.slice(0, -1);
54
+ process.stdout.clearLine(0);
55
+ process.stdout.cursorTo(0);
56
+ process.stdout.write(prompt + "*".repeat(input.length));
57
+ }
58
+ } else {
59
+ input += char;
60
+ process.stdout.write("*");
61
+ }
62
+ });
63
+ });
64
+ }
32
65
  function printBanner() {
33
66
  console.log(`
34
67
  ${c.purple}\u2588\u2588\u2588\u2588\u2588\u2588\u2557 ${c.reset}\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
@@ -388,6 +421,15 @@ async function runSetupWizard() {
388
421
  console.log(`
389
422
  ${c.green}\u2713${c.reset} Selected: ${selectedExchanges.map((e) => e.info?.name || e.id).join(", ")}
390
423
  `);
424
+ let existingConfigForSkip = {};
425
+ if (existsSync(CONFIG_PATH)) {
426
+ try {
427
+ const raw = readFileSync(CONFIG_PATH, "utf-8");
428
+ existingConfigForSkip = JSON.parse(raw);
429
+ } catch {
430
+ }
431
+ }
432
+ const existingExchanges = existingConfigForSkip.exchanges || {};
391
433
  const config = {
392
434
  exchanges: {},
393
435
  security: {
@@ -404,6 +446,17 @@ async function runSetupWizard() {
404
446
  ${c.white}${c.bold}STEP ${stepNum}/${totalSteps} \u2014 ${displayName} API KEYS${c.reset}
405
447
  ${c.gray}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${c.reset}
406
448
  `);
449
+ const existing = existingExchanges[exchange];
450
+ if (existing?.apiKey?.trim() && existing?.secret?.trim()) {
451
+ const maskedKey = `${existing.apiKey.slice(0, 5)}...${existing.apiKey.slice(-5)}`;
452
+ console.log(` ${c.green}\u2713${c.reset} ${displayName} already configured ${c.dim}(apiKey: ${maskedKey})${c.reset}`);
453
+ const keepAnswer = await question(` ${c.yellow}?${c.reset} Keep existing keys? ${c.dim}(Y/n)${c.reset}: `);
454
+ if (keepAnswer.toLowerCase() !== "n") {
455
+ config.exchanges[exchange] = existing;
456
+ console.log(` ${c.green}\u2713${c.reset} ${displayName} kept (existing keys)`);
457
+ continue;
458
+ }
459
+ }
407
460
  if (exchangeInfo) {
408
461
  console.log(` ${c.dim}Create API keys at:${c.reset} ${c.blue}${exchangeInfo.apiUrl}${c.reset}
409
462
  `);
@@ -427,12 +480,12 @@ async function runSetupWizard() {
427
480
  console.log(`
428
481
  ${c.dim}Paste your ${displayName} credentials:${c.reset}
429
482
  `);
430
- const apiKey = await question(` ${c.cyan}API Key:${c.reset} `);
431
- const secret = await question(` ${c.cyan}Secret:${c.reset} `);
483
+ const apiKey = await maskedQuestion(` ${c.cyan}API Key:${c.reset} `);
484
+ const secret = await maskedQuestion(` ${c.cyan}Secret:${c.reset} `);
432
485
  let password = "";
433
486
  const needsPassphrase = exchangeInfo?.needsPassphrase || ["coinbase", "kucoin", "okx", "bitget"].includes(exchange);
434
487
  if (needsPassphrase) {
435
- password = await question(` ${c.cyan}Passphrase:${c.reset} `);
488
+ password = await maskedQuestion(` ${c.cyan}Passphrase:${c.reset} `);
436
489
  }
437
490
  let testnet = false;
438
491
  const hasTestnet = exchangeInfo?.testnetUrl || ["binance", "bybit"].includes(exchange);
@@ -823,7 +876,7 @@ async function daemonStatus() {
823
876
  console.log("");
824
877
  }
825
878
  async function daemonRun() {
826
- const { startDaemon } = await import("./core-UZMUAQBA.js");
879
+ const { startDaemon } = await import("./core-IDBWLFHU.js");
827
880
  await startDaemon();
828
881
  }
829
882
  async function watchPrices(symbols) {
@@ -967,16 +1020,25 @@ async function setupNotifications(question) {
967
1020
  ${c.white}${c.bold}TELEGRAM SETUP${c.reset}
968
1021
  ${c.gray}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${c.reset}
969
1022
 
970
- ${c.cyan}1.${c.reset} Open Telegram and message ${c.white}@BotFather${c.reset}
1023
+ ${c.white}${c.bold}STEP 1 \u2014 CREATE YOUR BOT${c.reset}
1024
+ ${c.cyan}1.${c.reset} Open Telegram \u2192 search ${c.white}@BotFather${c.reset} \u2192 start a chat
971
1025
  ${c.cyan}2.${c.reset} Send: ${c.white}/newbot${c.reset}
972
- ${c.cyan}3.${c.reset} Follow prompts to create your bot
973
- ${c.cyan}4.${c.reset} Copy the ${c.white}HTTP API token${c.reset} BotFather gives you
974
- ${c.cyan}5.${c.reset} Send ${c.white}/start${c.reset} to your new bot
975
- ${c.cyan}6.${c.reset} Visit: ${c.blue}https://api.telegram.org/bot<TOKEN>/getUpdates${c.reset}
976
- Copy the ${c.white}chat.id${c.reset} number from the response
1026
+ ${c.cyan}3.${c.reset} Follow prompts (choose a name and username for your bot)
1027
+ ${c.cyan}4.${c.reset} BotFather gives you a ${c.white}Bot Token${c.reset} \u2014 it looks like this:
1028
+ ${c.dim}7481234567:AAHdqTcvCH1vGWJxfSeofSH2Y34H4ouyJe4${c.reset}
1029
+ Copy it.
1030
+
1031
+ ${c.white}${c.bold}STEP 2 \u2014 GET YOUR CHAT ID${c.reset}
1032
+ ${c.cyan}1.${c.reset} In Telegram, search for ${c.white}YOUR BOT${c.reset} by its @username
1033
+ ${c.cyan}2.${c.reset} Open the chat with it
1034
+ ${c.cyan}3.${c.reset} Send any message (type "hi" and hit send)
1035
+ ${c.cyan}4.${c.reset} Visit this URL in your browser (replace TOKEN with yours):
1036
+ ${c.blue}https://api.telegram.org/bot<TOKEN>/getUpdates${c.reset}
1037
+ ${c.cyan}5.${c.reset} Look for ${c.white}"chat": {"id": 1554736939 ...}${c.reset}
1038
+ That number is your ${c.white}Chat ID${c.reset}.
977
1039
 
978
1040
  `);
979
- const botToken = await question(` ${c.cyan}Bot token:${c.reset} `);
1041
+ const botToken = await maskedQuestion(` ${c.cyan}Bot token:${c.reset} `);
980
1042
  const chatId = await question(` ${c.cyan}Chat ID:${c.reset} `);
981
1043
  if (botToken.trim() && chatId.trim()) {
982
1044
  process.stdout.write(` Verifying... `);
@@ -1015,7 +1077,7 @@ async function setupNotifications(question) {
1015
1077
  ${c.cyan}3.${c.reset} Name it "OmniTrade" and copy the Webhook URL
1016
1078
 
1017
1079
  `);
1018
- const webhookUrl = await question(` ${c.cyan}Webhook URL:${c.reset} `);
1080
+ const webhookUrl = await maskedQuestion(` ${c.cyan}Webhook URL:${c.reset} `);
1019
1081
  if (webhookUrl.trim()) {
1020
1082
  process.stdout.write(` Verifying... `);
1021
1083
  try {
@@ -1131,16 +1193,29 @@ main().catch((error) => {
1131
1193
  async function runDashboard(args) {
1132
1194
  const symbolIdx = args.indexOf("--symbol");
1133
1195
  const chartSymbol = symbolIdx !== -1 ? (args[symbolIdx + 1] ?? "BTC").toUpperCase() : "BTC";
1196
+ const symbolsIdx = args.indexOf("--symbols");
1197
+ let customSymbols;
1198
+ if (symbolsIdx !== -1) {
1199
+ const symbolArgs = [];
1200
+ for (let i = symbolsIdx + 1; i < args.length; i++) {
1201
+ if (args[i].startsWith("--")) break;
1202
+ symbolArgs.push(args[i].toUpperCase());
1203
+ }
1204
+ if (symbolArgs.length > 0) customSymbols = symbolArgs;
1205
+ }
1134
1206
  const refreshIdx = args.indexOf("--refresh");
1135
1207
  const refreshSec = refreshIdx !== -1 ? parseInt(args[refreshIdx + 1] ?? "8", 10) : 8;
1208
+ const live = args.includes("--live");
1136
1209
  console.log(`${c.cyan}Starting OmniTrade Dashboard...${c.reset}`);
1137
- console.log(`${c.dim}Chart: ${chartSymbol}/USDT \u2502 Refresh: ${refreshSec}s \u2502 Press q to quit${c.reset}
1210
+ console.log(`${c.dim}Chart: ${chartSymbol}/USDT \u2502 Refresh: ${refreshSec}s \u2502 Mode: ${live ? "LIVE" : "paper"} \u2502 Press q to quit${c.reset}
1138
1211
  `);
1139
1212
  try {
1140
- const { startDashboard } = await import("./dashboard-6GJ4UVRY.js");
1213
+ const { startDashboard } = await import("./dashboard-LP5MRI2P.js");
1141
1214
  await startDashboard({
1142
1215
  chartSymbol,
1143
- refreshMs: refreshSec * 1e3
1216
+ refreshMs: refreshSec * 1e3,
1217
+ live,
1218
+ ...customSymbols ? { symbols: customSymbols } : {}
1144
1219
  });
1145
1220
  } catch (err) {
1146
1221
  console.error(`${c.red}Dashboard error:${c.reset}`, err.message);
@@ -1307,28 +1382,26 @@ ${c.red}\u2717 History error:${c.reset} ${err.message}
1307
1382
  }
1308
1383
  // ── paper reset ───────────────────────────────────────────
1309
1384
  case "reset": {
1310
- const { existsSync: existsSync2, unlinkSync } = await import("fs");
1311
- const { homedir: homedir2 } = await import("os");
1312
- const { join: join2 } = await import("path");
1313
- const walletPath = join2(homedir2(), ".omnitrade", "paper-wallet.json");
1314
- if (!existsSync2(walletPath)) {
1315
- console.log(`
1316
- ${c.yellow}\u26A0${c.reset} No paper wallet found.
1317
- `);
1318
- return;
1319
- }
1385
+ const { existsSync: fsExists, unlinkSync } = await import("fs");
1386
+ const { homedir: hd } = await import("os");
1387
+ const { join: pjoin } = await import("path");
1388
+ const walletPath = pjoin(hd(), ".omnitrade", "paper-wallet.json");
1320
1389
  const rl = (await import("readline")).createInterface({ input: process.stdin, output: process.stdout });
1321
- const answer = await new Promise((r) => rl.question(`
1322
- ${c.yellow}\u26A0 Reset paper wallet? This clears all trades and restarts with $10,000 (y/N): ${c.reset}`, r));
1390
+ const walletExists = fsExists(walletPath);
1391
+ const promptMsg = walletExists ? `
1392
+ ${c.yellow}\u26A0 Reset paper wallet? This clears all trades and restarts with $10,000 (y/N): ${c.reset}` : `
1393
+ ${c.yellow}? No paper wallet found. Create a fresh $10,000 wallet? (Y/n): ${c.reset}`;
1394
+ const answer = await new Promise((r) => rl.question(promptMsg, r));
1323
1395
  rl.close();
1324
- if (answer.toLowerCase() === "y") {
1325
- unlinkSync(walletPath);
1396
+ const confirmed = walletExists ? answer.toLowerCase() === "y" : answer.toLowerCase() !== "n";
1397
+ if (confirmed) {
1398
+ if (walletExists) unlinkSync(walletPath);
1326
1399
  loadWallet();
1327
1400
  console.log(`
1328
1401
  ${c.green}\u2713 Paper wallet reset to $10,000 USDT${c.reset}
1329
1402
  `);
1330
1403
  } else {
1331
- console.log(`${c.dim}Reset cancelled.${c.reset}
1404
+ console.log(`${c.dim}Cancelled.${c.reset}
1332
1405
  `);
1333
1406
  }
1334
1407
  break;
@@ -112,6 +112,7 @@ async function sendNotification(config, title, message) {
112
112
  // src/daemon/core.ts
113
113
  var OMNITRADE_DIR = join(homedir(), ".omnitrade");
114
114
  var ALERTS_FILE = join(OMNITRADE_DIR, "alerts.json");
115
+ var DCA_FILE = join(OMNITRADE_DIR, "dca.json");
115
116
  var LOG_FILE = join(OMNITRADE_DIR, "daemon.log");
116
117
  function log(message) {
117
118
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
@@ -140,6 +141,96 @@ async function saveAlerts(data) {
140
141
  await fs.mkdir(OMNITRADE_DIR, { recursive: true });
141
142
  await fs.writeFile(ALERTS_FILE, JSON.stringify(data, null, 2));
142
143
  }
144
+ async function loadDCAConfigs() {
145
+ if (!existsSync(DCA_FILE)) {
146
+ return { configs: [] };
147
+ }
148
+ try {
149
+ const raw = await fs.readFile(DCA_FILE, "utf-8");
150
+ return JSON.parse(raw);
151
+ } catch {
152
+ return { configs: [] };
153
+ }
154
+ }
155
+ async function saveDCAConfigs(data) {
156
+ await fs.mkdir(OMNITRADE_DIR, { recursive: true });
157
+ await fs.writeFile(DCA_FILE, JSON.stringify(data, null, 2));
158
+ }
159
+ function getDCAFrequencyMs(frequency) {
160
+ const intervals = {
161
+ hourly: 60 * 60 * 1e3,
162
+ daily: 24 * 60 * 60 * 1e3,
163
+ weekly: 7 * 24 * 60 * 60 * 1e3,
164
+ monthly: 30 * 24 * 60 * 60 * 1e3
165
+ };
166
+ return intervals[frequency];
167
+ }
168
+ function isDCADue(dca, now) {
169
+ if (!dca.enabled) return false;
170
+ if (!dca.lastExecuted) return true;
171
+ return now - dca.lastExecuted >= getDCAFrequencyMs(dca.frequency);
172
+ }
173
+ async function pollAndCheckDCAs(exchanges, config) {
174
+ const data = await loadDCAConfigs();
175
+ const now = Date.now();
176
+ const dueDCAs = data.configs.filter((d) => isDCADue(d, now));
177
+ if (dueDCAs.length === 0) {
178
+ log(`DCA check \u2014 no orders due`);
179
+ return;
180
+ }
181
+ log(`DCA check \u2014 ${dueDCAs.length} order(s) due`);
182
+ for (const dca of dueDCAs) {
183
+ const exchange = exchanges.get(dca.exchange);
184
+ if (!exchange) {
185
+ log(` \u26A0 DCA ${dca.id}: exchange ${dca.exchange} not available \u2014 skipping`);
186
+ continue;
187
+ }
188
+ try {
189
+ const ticker = await exchange.fetchTicker(dca.symbol);
190
+ const price = ticker.last ?? 0;
191
+ if (price <= 0) {
192
+ log(` \u26A0 DCA ${dca.id}: invalid price for ${dca.symbol} \u2014 skipping`);
193
+ continue;
194
+ }
195
+ const exchCfg = config.exchanges[dca.exchange];
196
+ const hasCredentials = !!(exchCfg?.apiKey && exchCfg?.secret);
197
+ let spent = dca.amountUSD;
198
+ if (hasCredentials) {
199
+ try {
200
+ const amount = dca.amountUSD / price;
201
+ const order = await exchange.createMarketBuyOrder(dca.symbol, amount);
202
+ spent = order.cost ?? dca.amountUSD;
203
+ log(` \u2713 DCA ${dca.id}: REAL buy ${dca.symbol} \u2014 $${spent.toFixed(2)} at $${price.toFixed(2)} (order: ${order.id})`);
204
+ } catch (orderErr) {
205
+ log(` \u26A0 DCA ${dca.id}: real order failed, logging as simulated: ${orderErr.message}`);
206
+ }
207
+ } else {
208
+ log(` \u2713 DCA ${dca.id}: SIMULATED buy ${dca.symbol} \u2014 $${dca.amountUSD.toFixed(2)} at $${price.toFixed(2)} [no credentials]`);
209
+ }
210
+ dca.lastExecuted = now;
211
+ dca.totalExecutions += 1;
212
+ dca.totalSpent += spent;
213
+ const baseAsset = dca.symbol.split("/")[0] ?? dca.symbol;
214
+ const title = `OmniTrade DCA: ${baseAsset}`;
215
+ const message = `DCA executed: bought $${dca.amountUSD} of ${baseAsset} at $${price.toFixed(2)} on ${dca.exchange}`;
216
+ const results = await sendNotification(config.notifications, title, message);
217
+ for (const result of results) {
218
+ if (result.success) {
219
+ log(` \u2713 DCA notification sent via ${result.channel}`);
220
+ } else {
221
+ log(` \u2717 DCA notification failed via ${result.channel}: ${result.error}`);
222
+ }
223
+ }
224
+ if (results.length === 0) {
225
+ log(` \u2139 DCA: no notification channels configured`);
226
+ }
227
+ } catch (err) {
228
+ log(` \u2717 DCA ${dca.id}: error \u2014 ${err.message}`);
229
+ }
230
+ }
231
+ await saveDCAConfigs(data);
232
+ log(`DCA check complete \u2014 ${dueDCAs.length} processed`);
233
+ }
143
234
  function createPublicExchange(name) {
144
235
  const id = name.toLowerCase();
145
236
  if (!ccxt.exchanges.includes(id)) return null;
@@ -244,13 +335,23 @@ async function startDaemon() {
244
335
  try {
245
336
  await pollAndCheckAlerts(exchanges, config);
246
337
  } catch (err) {
247
- log(`Poll error: ${err.message}`);
338
+ log(`Alert poll error: ${err.message}`);
339
+ }
340
+ try {
341
+ await pollAndCheckDCAs(exchanges, config);
342
+ } catch (err) {
343
+ log(`DCA poll error: ${err.message}`);
248
344
  }
249
345
  const timer = setInterval(async () => {
250
346
  try {
251
347
  await pollAndCheckAlerts(exchanges, config);
252
348
  } catch (err) {
253
- log(`Poll error: ${err.message}`);
349
+ log(`Alert poll error: ${err.message}`);
350
+ }
351
+ try {
352
+ await pollAndCheckDCAs(exchanges, config);
353
+ } catch (err) {
354
+ log(`DCA poll error: ${err.message}`);
254
355
  }
255
356
  }, pollInterval);
256
357
  timer.unref();
@@ -0,0 +1,480 @@
1
+ import {
2
+ fetch24hTicker,
3
+ fetchKlines,
4
+ getPortfolioSummary,
5
+ loadWallet
6
+ } from "./chunk-FTMAZW2Z.js";
7
+
8
+ // src/dashboard/index.ts
9
+ import { createRequire } from "module";
10
+ import { existsSync, readFileSync } from "fs";
11
+ import { homedir } from "os";
12
+ import { join } from "path";
13
+ var _require = createRequire(import.meta.url);
14
+ var _pkg = _require("../package.json");
15
+ var VERSION = _pkg.version;
16
+ var DEFAULT_SYMBOLS = ["BTC", "ETH", "SOL", "BNB", "XRP", "ADA", "DOGE", "AVAX"];
17
+ var DEFAULT_CONFIG = {
18
+ symbols: DEFAULT_SYMBOLS,
19
+ chartSymbol: "BTC",
20
+ refreshMs: 8e3,
21
+ live: false
22
+ };
23
+ function loadOmniConfig() {
24
+ const configPath = join(homedir(), ".omnitrade", "config.json");
25
+ if (!existsSync(configPath)) return null;
26
+ try {
27
+ return JSON.parse(readFileSync(configPath, "utf-8"));
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+ async function fetchLiveBalances() {
33
+ const omniConfig = loadOmniConfig();
34
+ if (!omniConfig?.exchanges) return [];
35
+ const ccxt = await import("ccxt");
36
+ const results = [];
37
+ for (const [name, cfg] of Object.entries(omniConfig.exchanges)) {
38
+ if (!cfg.apiKey || !cfg.secret) continue;
39
+ try {
40
+ const ExchangeClass = ccxt.default[name];
41
+ if (!ExchangeClass) continue;
42
+ const exchange = new ExchangeClass({
43
+ apiKey: cfg.apiKey,
44
+ secret: cfg.secret,
45
+ password: cfg.password,
46
+ enableRateLimit: true
47
+ });
48
+ if (cfg.testnet) exchange.setSandboxMode(true);
49
+ const balance = await exchange.fetchBalance();
50
+ const balanceTotal = balance.total;
51
+ for (const [asset, total] of Object.entries(balanceTotal)) {
52
+ if (!total || total <= 0) continue;
53
+ let usdValue = 0;
54
+ let price = 0;
55
+ if (asset === "USDT" || asset === "USD" || asset === "BUSD" || asset === "USDC") {
56
+ price = 1;
57
+ usdValue = total;
58
+ } else {
59
+ try {
60
+ const ticker = await exchange.fetchTicker(`${asset}/USDT`);
61
+ price = ticker.last ?? 0;
62
+ usdValue = total * price;
63
+ } catch {
64
+ }
65
+ }
66
+ const freeBalance = balance.free[asset] ?? 0;
67
+ results.push({
68
+ exchange: name,
69
+ asset,
70
+ free: freeBalance,
71
+ total,
72
+ usdValue,
73
+ price
74
+ });
75
+ }
76
+ } catch {
77
+ }
78
+ }
79
+ return results;
80
+ }
81
+ async function fetchMultiExchangePrices(symbol, exchangeNames) {
82
+ const ccxt = await import("ccxt");
83
+ const omniConfig = loadOmniConfig();
84
+ const results = [];
85
+ for (const name of exchangeNames) {
86
+ const cfg = omniConfig?.exchanges?.[name];
87
+ try {
88
+ const ExchangeClass = ccxt.default[name];
89
+ if (!ExchangeClass) continue;
90
+ const exchange = new ExchangeClass({
91
+ ...cfg?.apiKey ? { apiKey: cfg.apiKey, secret: cfg.secret, password: cfg.password } : {},
92
+ enableRateLimit: true
93
+ });
94
+ const ticker = await exchange.fetchTicker(`${symbol}/USDT`);
95
+ if (ticker.last && ticker.last > 0) {
96
+ results.push({ exchange: name, price: ticker.last });
97
+ }
98
+ } catch {
99
+ }
100
+ }
101
+ return results;
102
+ }
103
+ async function startDashboard(config = {}) {
104
+ const cfg = { ...DEFAULT_CONFIG, ...config };
105
+ const omniConfig = loadOmniConfig();
106
+ const configuredExchangeNames = omniConfig?.exchanges ? Object.keys(omniConfig.exchanges) : [];
107
+ const showMultiExchange = cfg.live && configuredExchangeNames.length > 1;
108
+ const blessed = (await import("blessed")).default;
109
+ const contrib = (await import("blessed-contrib")).default;
110
+ const screen = blessed.screen({
111
+ smartCSR: true,
112
+ fullUnicode: true,
113
+ title: "OmniTrade Dashboard"
114
+ });
115
+ const grid = new contrib.grid({ rows: 12, cols: 12, screen });
116
+ const priceTable = grid.set(0, 0, 6, 7, contrib.table, {
117
+ label: " LIVE PRICES ",
118
+ keys: true,
119
+ vi: true,
120
+ mouse: true,
121
+ style: {
122
+ header: { fg: "cyan", bold: true },
123
+ cell: { fg: "white", selected: { fg: "black", bg: "cyan" } },
124
+ border: { fg: "cyan" },
125
+ label: { fg: "cyan" }
126
+ },
127
+ columnSpacing: 2,
128
+ columnWidth: showMultiExchange ? [10, 14, 9, 12, 12] : [10, 14, 9, 16]
129
+ });
130
+ const lineChart = grid.set(0, 7, 6, 5, contrib.line, {
131
+ label: ` ${cfg.chartSymbol}/USDT \u2014 24h `,
132
+ showLegend: false,
133
+ wholeNumbersOnly: false,
134
+ xLabelPadding: 2,
135
+ xPadding: 5,
136
+ style: {
137
+ line: "green",
138
+ text: "white",
139
+ baseline: "black",
140
+ border: { fg: "cyan" },
141
+ label: { fg: "cyan" }
142
+ }
143
+ });
144
+ const portfolioTable = grid.set(6, 0, 4, 12, contrib.table, {
145
+ label: " PORTFOLIO ",
146
+ keys: false,
147
+ style: {
148
+ header: { fg: "yellow", bold: true },
149
+ cell: { fg: "white" },
150
+ border: { fg: "yellow" },
151
+ label: { fg: "yellow" }
152
+ },
153
+ columnSpacing: 2,
154
+ columnWidth: cfg.live ? [10, 10, 14, 14, 14, 10] : [10, 14, 14, 14, 16, 14, 10]
155
+ });
156
+ const statusBar = grid.set(10, 0, 2, 12, blessed.box, {
157
+ tags: true,
158
+ style: {
159
+ fg: "white",
160
+ bg: "black",
161
+ border: { fg: "gray" }
162
+ },
163
+ border: { type: "line" },
164
+ padding: { left: 1, right: 1, top: 0, bottom: 0 }
165
+ });
166
+ let panelToggle = 0;
167
+ const panels = [priceTable, lineChart, portfolioTable];
168
+ function applyPanelToggle() {
169
+ if (panelToggle === 0) {
170
+ panels.forEach((p) => p.show());
171
+ } else if (panelToggle === 1) {
172
+ priceTable.show();
173
+ lineChart.show();
174
+ portfolioTable.hide();
175
+ } else {
176
+ priceTable.hide();
177
+ lineChart.hide();
178
+ portfolioTable.show();
179
+ }
180
+ screen.render();
181
+ }
182
+ screen.key(["q", "C-c"], () => {
183
+ screen.destroy();
184
+ process.exit(0);
185
+ });
186
+ screen.key(["t"], () => {
187
+ panelToggle = (panelToggle + 1) % 3;
188
+ applyPanelToggle();
189
+ });
190
+ screen.key(["tab"], () => {
191
+ screen.focusNext();
192
+ screen.render();
193
+ });
194
+ let lastUpdate = "never";
195
+ let connectionStatus = "\u25CF CONNECTING";
196
+ let connectionColor = "{yellow-fg}";
197
+ function renderStatus() {
198
+ const modeStr = cfg.live ? "{green-fg}LIVE{/}" : "{yellow-fg}PAPER{/}";
199
+ const helpStr = "{gray-fg}q{/} quit {gray-fg}t{/} toggle panels {gray-fg}Tab{/} navigate";
200
+ statusBar.setContent(
201
+ `${connectionColor}${connectionStatus}{/} {gray-fg}\u2502{/} Mode: ${modeStr} {gray-fg}\u2502{/} {white-fg}Updated: ${lastUpdate}{/} {gray-fg}\u2502{/} ${helpStr} {gray-fg}\u2502{/} {cyan-fg}OmniTrade v${VERSION}{/}`
202
+ );
203
+ screen.render();
204
+ }
205
+ async function refreshPrices() {
206
+ try {
207
+ const tickers = await Promise.allSettled(
208
+ cfg.symbols.map((s) => fetch24hTicker(s))
209
+ );
210
+ let headers;
211
+ const rows = [];
212
+ if (showMultiExchange) {
213
+ headers = ["Symbol", "Price", "24h %", "Binance", "Best"];
214
+ for (let i = 0; i < cfg.symbols.length; i++) {
215
+ const result = tickers[i];
216
+ const sym = cfg.symbols[i];
217
+ if (result?.status === "fulfilled") {
218
+ const t = result.value;
219
+ const price = fmtTablePrice(t.lastPrice);
220
+ const pct = t.priceChangePercent;
221
+ const pctStr = `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`;
222
+ rows.push([sym, price, pctStr, price, "\u2190best"]);
223
+ } else {
224
+ rows.push([sym, "---", "---", "---", "---"]);
225
+ }
226
+ }
227
+ } else {
228
+ headers = ["Symbol", "Price", "24h %", "Volume (USDT)"];
229
+ for (let i = 0; i < cfg.symbols.length; i++) {
230
+ const result = tickers[i];
231
+ const sym = cfg.symbols[i];
232
+ if (result?.status === "fulfilled") {
233
+ const t = result.value;
234
+ const price = fmtTablePrice(t.lastPrice);
235
+ const pct = t.priceChangePercent;
236
+ const pctStr = `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`;
237
+ const vol = fmtVolume(t.quoteVolume);
238
+ rows.push([sym, price, pctStr, vol]);
239
+ } else {
240
+ rows.push([sym, "---", "---", "---"]);
241
+ }
242
+ }
243
+ }
244
+ priceTable.setData({ headers, data: rows });
245
+ connectionStatus = "\u25CF CONNECTED";
246
+ connectionColor = "{green-fg}";
247
+ lastUpdate = (/* @__PURE__ */ new Date()).toLocaleTimeString();
248
+ } catch {
249
+ connectionStatus = "\u25CF RECONNECTING";
250
+ connectionColor = "{red-fg}";
251
+ }
252
+ renderStatus();
253
+ }
254
+ async function refreshMultiExchangePrices() {
255
+ if (!showMultiExchange || configuredExchangeNames.length < 2) return;
256
+ try {
257
+ const headers = ["Symbol", "Price", "24h %", "Exchange", "Note"];
258
+ const rows = [];
259
+ for (const sym of cfg.symbols.slice(0, 4)) {
260
+ let mainPrice = 0;
261
+ try {
262
+ const t = await fetch24hTicker(sym);
263
+ mainPrice = t.lastPrice;
264
+ const pct = t.priceChangePercent;
265
+ const pctStr = `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`;
266
+ rows.push([sym, fmtTablePrice(mainPrice), pctStr, "binance", ""]);
267
+ } catch {
268
+ rows.push([sym, "---", "---", "---", ""]);
269
+ }
270
+ const otherExchanges = configuredExchangeNames.filter((e) => e !== "binance").slice(0, 2);
271
+ if (otherExchanges.length > 0) {
272
+ const exchangePrices = await fetchMultiExchangePrices(sym, otherExchanges);
273
+ let bestPrice = mainPrice;
274
+ let bestExchange = "binance";
275
+ for (const ep of exchangePrices) {
276
+ if (ep.price < bestPrice || bestPrice === 0) {
277
+ bestPrice = ep.price;
278
+ bestExchange = ep.exchange;
279
+ }
280
+ rows.push(["", fmtTablePrice(ep.price), "", ep.exchange, ep.exchange === bestExchange ? "\u2190 best" : ""]);
281
+ }
282
+ let bestIdx = -1;
283
+ for (let ri = rows.length - 1; ri >= 0; ri--) {
284
+ if (rows[ri]?.[3] === bestExchange) {
285
+ bestIdx = ri;
286
+ break;
287
+ }
288
+ }
289
+ if (bestIdx >= 0 && rows[bestIdx]) {
290
+ rows[bestIdx][4] = "\u2190 best";
291
+ }
292
+ }
293
+ }
294
+ priceTable.setData({ headers, data: rows });
295
+ screen.render();
296
+ } catch {
297
+ }
298
+ }
299
+ async function refreshChart() {
300
+ try {
301
+ const klines = await fetchKlines(cfg.chartSymbol, "1h", 24);
302
+ if (klines.length < 2) return;
303
+ const x = klines.map((k) => {
304
+ const d = new Date(k.time);
305
+ return `${d.getHours().toString().padStart(2, "0")}:00`;
306
+ });
307
+ const y = klines.map((k) => k.close);
308
+ const firstPrice = y[0];
309
+ const lastPrice = y[y.length - 1];
310
+ const isUp = lastPrice >= firstPrice;
311
+ lineChart.options.label = ` ${cfg.chartSymbol}/USDT \u2014 24h ${isUp ? "\u25B2" : "\u25BC"} ${fmtTablePrice(lastPrice)} `;
312
+ lineChart.options.style.line = isUp ? "green" : "red";
313
+ lineChart.setData([
314
+ {
315
+ title: cfg.chartSymbol,
316
+ x,
317
+ y,
318
+ style: { line: isUp ? "green" : "red" }
319
+ }
320
+ ]);
321
+ } catch {
322
+ }
323
+ screen.render();
324
+ }
325
+ async function refreshPortfolio() {
326
+ try {
327
+ if (cfg.live) {
328
+ await refreshLivePortfolio();
329
+ } else {
330
+ await refreshPaperPortfolio();
331
+ }
332
+ } catch {
333
+ }
334
+ screen.render();
335
+ }
336
+ async function refreshPaperPortfolio() {
337
+ const wallet = loadWallet();
338
+ const summary = await getPortfolioSummary(wallet);
339
+ const headers = ["Asset", "Amount", "Price", "Value", "Avg Buy", "P&L", "Alloc %"];
340
+ const rows = [];
341
+ const usdtPct = summary.totalValue > 0 ? (summary.usdtBalance / summary.totalValue * 100).toFixed(1) : "0.0";
342
+ rows.push([
343
+ "USDT",
344
+ summary.usdtBalance.toFixed(2),
345
+ "$1.00",
346
+ `$${summary.usdtBalance.toFixed(2)}`,
347
+ "---",
348
+ "---",
349
+ `${usdtPct}%`
350
+ ]);
351
+ for (const h of summary.holdings) {
352
+ const pnlSign2 = h.pnl >= 0 ? "+" : "";
353
+ rows.push([
354
+ h.asset,
355
+ fmtAmount(h.amount),
356
+ fmtTablePrice(h.price),
357
+ `$${h.value.toFixed(2)}`,
358
+ fmtTablePrice(h.avgBuyPrice),
359
+ `${pnlSign2}$${h.pnl.toFixed(2)}`,
360
+ `${h.allocation.toFixed(1)}%`
361
+ ]);
362
+ }
363
+ const pnlSign = summary.totalPnl >= 0 ? "+" : "";
364
+ portfolioTable.setData({ headers, data: rows });
365
+ portfolioTable.options.label = ` PORTFOLIO (paper) Total: $${summary.totalValue.toFixed(2)} \u2502 P&L: ${pnlSign}$${Math.abs(summary.totalPnl).toFixed(2)} (${pnlSign}${summary.totalPnlPct.toFixed(2)}%) `;
366
+ }
367
+ async function refreshLivePortfolio() {
368
+ const balances = await fetchLiveBalances();
369
+ if (balances.length === 0) {
370
+ portfolioTable.setData({
371
+ headers: ["Asset", "Exchange", "Amount", "Price", "USD Value", "Alloc %"],
372
+ data: [["No live data", "---", "---", "---", "---", "---"]]
373
+ });
374
+ portfolioTable.options.label = " PORTFOLIO (live) \u2014 no exchange data ";
375
+ return;
376
+ }
377
+ const aggregated = /* @__PURE__ */ new Map();
378
+ const totalUSD = balances.reduce((s, b) => s + b.usdValue, 0);
379
+ for (const b of balances) {
380
+ const existing = aggregated.get(b.asset);
381
+ if (existing) {
382
+ existing.total += b.total;
383
+ existing.usdValue += b.usdValue;
384
+ existing.exchanges.push(b.exchange);
385
+ } else {
386
+ aggregated.set(b.asset, { total: b.total, usdValue: b.usdValue, exchanges: [b.exchange] });
387
+ }
388
+ }
389
+ const headers = ["Asset", "Exchange", "Amount", "Price", "USD Value", "Alloc %"];
390
+ const rows = [];
391
+ const sorted = Array.from(aggregated.entries()).sort((a, b) => b[1].usdValue - a[1].usdValue);
392
+ for (const [asset, data] of sorted) {
393
+ const alloc = totalUSD > 0 ? (data.usdValue / totalUSD * 100).toFixed(1) : "0.0";
394
+ const assetBalances = balances.filter((b) => b.asset === asset);
395
+ if (assetBalances.length === 1) {
396
+ const b = assetBalances[0];
397
+ rows.push([
398
+ asset,
399
+ b.exchange,
400
+ fmtAmount(data.total),
401
+ b.price > 0 ? fmtTablePrice(b.price) : "---",
402
+ `$${data.usdValue.toFixed(2)}`,
403
+ `${alloc}%`
404
+ ]);
405
+ } else {
406
+ rows.push([
407
+ asset,
408
+ `(${assetBalances.length} exch)`,
409
+ fmtAmount(data.total),
410
+ "---",
411
+ `$${data.usdValue.toFixed(2)}`,
412
+ `${alloc}%`
413
+ ]);
414
+ for (const b of assetBalances) {
415
+ rows.push([
416
+ "",
417
+ ` ${b.exchange}`,
418
+ fmtAmount(b.total),
419
+ b.price > 0 ? fmtTablePrice(b.price) : "---",
420
+ `$${b.usdValue.toFixed(2)}`,
421
+ ""
422
+ ]);
423
+ }
424
+ }
425
+ }
426
+ portfolioTable.setData({ headers, data: rows });
427
+ portfolioTable.options.label = ` PORTFOLIO (live) Total: $${totalUSD.toFixed(2)} across ${configuredExchangeNames.length} exchange(s) `;
428
+ }
429
+ function fmtTablePrice(p) {
430
+ if (p >= 1e4) return `$${p.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
431
+ if (p >= 100) return `$${p.toFixed(2)}`;
432
+ if (p >= 1) return `$${p.toFixed(4)}`;
433
+ return `$${p.toFixed(6)}`;
434
+ }
435
+ function fmtVolume(v) {
436
+ if (v >= 1e9) return `$${(v / 1e9).toFixed(2)}B`;
437
+ if (v >= 1e6) return `$${(v / 1e6).toFixed(1)}M`;
438
+ if (v >= 1e3) return `$${(v / 1e3).toFixed(1)}K`;
439
+ return `$${v.toFixed(2)}`;
440
+ }
441
+ function fmtAmount(a) {
442
+ if (a >= 1e3) return a.toFixed(2);
443
+ if (a >= 1) return a.toFixed(4);
444
+ if (a >= 1e-3) return a.toFixed(6);
445
+ return a.toFixed(8);
446
+ }
447
+ renderStatus();
448
+ screen.render();
449
+ priceTable.setData({ headers: ["Symbol", "Price", "24h %", "Volume"], data: [["Loading...", "", "", ""]] });
450
+ portfolioTable.setData({ headers: ["Asset", "Amount", "Price", "Value", "Avg Buy", "P&L", "Alloc %"], data: [["Loading...", "", "", "", "", "", ""]] });
451
+ screen.render();
452
+ await Promise.all([refreshPrices(), refreshChart(), refreshPortfolio()]);
453
+ if (showMultiExchange) {
454
+ refreshMultiExchangePrices().catch(() => {
455
+ });
456
+ }
457
+ const priceTimer = setInterval(async () => {
458
+ await refreshPrices();
459
+ if (showMultiExchange) {
460
+ refreshMultiExchangePrices().catch(() => {
461
+ });
462
+ }
463
+ }, cfg.refreshMs);
464
+ const chartTimer = setInterval(async () => {
465
+ await refreshChart();
466
+ }, cfg.refreshMs * 3);
467
+ const portfolioTimer = setInterval(async () => {
468
+ await refreshPortfolio();
469
+ }, cfg.refreshMs * 2);
470
+ screen.on("destroy", () => {
471
+ clearInterval(priceTimer);
472
+ clearInterval(chartTimer);
473
+ clearInterval(portfolioTimer);
474
+ });
475
+ priceTable.focus();
476
+ screen.render();
477
+ }
478
+ export {
479
+ startDashboard
480
+ };
package/dist/index.js CHANGED
@@ -3015,7 +3015,10 @@ function registerConditionalOrderTools(server, exchangeManager, config) {
3015
3015
  }
3016
3016
 
3017
3017
  // src/index.ts
3018
- var VERSION = "0.9.4";
3018
+ import { createRequire } from "module";
3019
+ var _require = createRequire(import.meta.url);
3020
+ var _pkg = _require("../package.json");
3021
+ var VERSION = _pkg.version;
3019
3022
  function showBanner() {
3020
3023
  const purple = "\x1B[35m";
3021
3024
  const reset = "\x1B[0m";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnitrade-mcp",
3
- "version": "0.9.4",
3
+ "version": "0.9.9",
4
4
  "description": "Multi-exchange AI trading via MCP. 107 exchanges. One AI.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,250 +0,0 @@
1
- import {
2
- fetch24hTicker,
3
- fetchKlines,
4
- getPortfolioSummary,
5
- loadWallet
6
- } from "./chunk-FTMAZW2Z.js";
7
-
8
- // src/dashboard/index.ts
9
- var DEFAULT_CONFIG = {
10
- symbols: ["BTC", "ETH", "SOL", "BNB", "XRP", "ADA", "DOGE", "AVAX"],
11
- chartSymbol: "BTC",
12
- refreshMs: 8e3
13
- };
14
- async function startDashboard(config = {}) {
15
- const cfg = { ...DEFAULT_CONFIG, ...config };
16
- const blessed = (await import("blessed")).default;
17
- const contrib = (await import("blessed-contrib")).default;
18
- const screen = blessed.screen({
19
- smartCSR: true,
20
- fullUnicode: true,
21
- title: "OmniTrade Dashboard"
22
- });
23
- const grid = new contrib.grid({ rows: 12, cols: 12, screen });
24
- const priceTable = grid.set(0, 0, 6, 7, contrib.table, {
25
- label: " LIVE PRICES ",
26
- keys: true,
27
- vi: true,
28
- mouse: true,
29
- style: {
30
- header: { fg: "cyan", bold: true },
31
- cell: { fg: "white", selected: { fg: "black", bg: "cyan" } },
32
- border: { fg: "cyan" },
33
- label: { fg: "cyan" }
34
- },
35
- columnSpacing: 2,
36
- columnWidth: [10, 14, 9, 16]
37
- });
38
- const lineChart = grid.set(0, 7, 6, 5, contrib.line, {
39
- label: ` ${cfg.chartSymbol}/USDT \u2014 24h `,
40
- showLegend: false,
41
- wholeNumbersOnly: false,
42
- xLabelPadding: 2,
43
- xPadding: 5,
44
- style: {
45
- line: "green",
46
- text: "white",
47
- baseline: "black",
48
- border: { fg: "cyan" },
49
- label: { fg: "cyan" }
50
- }
51
- });
52
- const portfolioTable = grid.set(6, 0, 4, 12, contrib.table, {
53
- label: " PORTFOLIO ",
54
- keys: false,
55
- style: {
56
- header: { fg: "yellow", bold: true },
57
- cell: { fg: "white" },
58
- border: { fg: "yellow" },
59
- label: { fg: "yellow" }
60
- },
61
- columnSpacing: 2,
62
- columnWidth: [10, 14, 14, 14, 16, 14, 10]
63
- });
64
- const statusBar = grid.set(10, 0, 2, 12, blessed.box, {
65
- tags: true,
66
- style: {
67
- fg: "white",
68
- bg: "black",
69
- border: { fg: "gray" }
70
- },
71
- border: { type: "line" },
72
- padding: { left: 1, right: 1, top: 0, bottom: 0 }
73
- });
74
- let panelToggle = 0;
75
- const panels = [priceTable, lineChart, portfolioTable];
76
- function applyPanelToggle() {
77
- if (panelToggle === 0) {
78
- panels.forEach((p) => p.show());
79
- } else if (panelToggle === 1) {
80
- priceTable.show();
81
- lineChart.show();
82
- portfolioTable.hide();
83
- } else {
84
- priceTable.hide();
85
- lineChart.hide();
86
- portfolioTable.show();
87
- }
88
- screen.render();
89
- }
90
- screen.key(["q", "C-c"], () => {
91
- screen.destroy();
92
- process.exit(0);
93
- });
94
- screen.key(["t"], () => {
95
- panelToggle = (panelToggle + 1) % 3;
96
- applyPanelToggle();
97
- });
98
- screen.key(["tab"], () => {
99
- screen.focusNext();
100
- screen.render();
101
- });
102
- let lastUpdate = "never";
103
- let connectionStatus = "\u25CF CONNECTING";
104
- let connectionColor = "{yellow-fg}";
105
- function renderStatus() {
106
- const helpStr = "{gray-fg}q{/} quit {gray-fg}t{/} toggle panels {gray-fg}Tab{/} navigate";
107
- statusBar.setContent(
108
- `${connectionColor}${connectionStatus}{/} {gray-fg}\u2502{/} {white-fg}Updated: ${lastUpdate}{/} {gray-fg}\u2502{/} ${helpStr} {gray-fg}\u2502{/} {cyan-fg}OmniTrade v0.8.1{/}`
109
- );
110
- screen.render();
111
- }
112
- async function refreshPrices() {
113
- try {
114
- const tickers = await Promise.allSettled(
115
- cfg.symbols.map((s) => fetch24hTicker(s))
116
- );
117
- const headers = ["Symbol", "Price", "24h %", "Volume (USDT)"];
118
- const rows = [];
119
- for (let i = 0; i < cfg.symbols.length; i++) {
120
- const result = tickers[i];
121
- const sym = cfg.symbols[i];
122
- if (result?.status === "fulfilled") {
123
- const t = result.value;
124
- const price = fmtTablePrice(t.lastPrice);
125
- const pct = t.priceChangePercent;
126
- const pctStr = `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`;
127
- const vol = fmtVolume(t.quoteVolume);
128
- rows.push([sym, price, pctStr, vol]);
129
- } else {
130
- rows.push([sym, "---", "---", "---"]);
131
- }
132
- }
133
- priceTable.setData({ headers, data: rows });
134
- connectionStatus = "\u25CF CONNECTED";
135
- connectionColor = "{green-fg}";
136
- lastUpdate = (/* @__PURE__ */ new Date()).toLocaleTimeString();
137
- } catch {
138
- connectionStatus = "\u25CF RECONNECTING";
139
- connectionColor = "{red-fg}";
140
- }
141
- renderStatus();
142
- }
143
- async function refreshChart() {
144
- try {
145
- const klines = await fetchKlines(cfg.chartSymbol, "1h", 24);
146
- if (klines.length < 2) return;
147
- const x = klines.map((k) => {
148
- const d = new Date(k.time);
149
- return `${d.getHours().toString().padStart(2, "0")}:00`;
150
- });
151
- const y = klines.map((k) => k.close);
152
- const firstPrice = y[0];
153
- const lastPrice = y[y.length - 1];
154
- const isUp = lastPrice >= firstPrice;
155
- lineChart.options.label = ` ${cfg.chartSymbol}/USDT \u2014 24h ${isUp ? "\u25B2" : "\u25BC"} ${fmtTablePrice(lastPrice)} `;
156
- lineChart.options.style.line = isUp ? "green" : "red";
157
- lineChart.setData([
158
- {
159
- title: cfg.chartSymbol,
160
- x,
161
- y,
162
- style: { line: isUp ? "green" : "red" }
163
- }
164
- ]);
165
- } catch {
166
- }
167
- screen.render();
168
- }
169
- async function refreshPortfolio() {
170
- try {
171
- const wallet = loadWallet();
172
- const summary = await getPortfolioSummary(wallet);
173
- const headers = ["Asset", "Amount", "Price", "Value", "Avg Buy", "P&L", "Alloc %"];
174
- const rows = [];
175
- const usdtPct = summary.totalValue > 0 ? (summary.usdtBalance / summary.totalValue * 100).toFixed(1) : "0.0";
176
- rows.push([
177
- "USDT",
178
- summary.usdtBalance.toFixed(2),
179
- "$1.00",
180
- `$${summary.usdtBalance.toFixed(2)}`,
181
- "---",
182
- "---",
183
- `${usdtPct}%`
184
- ]);
185
- for (const h of summary.holdings) {
186
- const pnlSign2 = h.pnl >= 0 ? "+" : "";
187
- rows.push([
188
- h.asset,
189
- fmtAmount(h.amount),
190
- fmtTablePrice(h.price),
191
- `$${h.value.toFixed(2)}`,
192
- fmtTablePrice(h.avgBuyPrice),
193
- `${pnlSign2}$${h.pnl.toFixed(2)}`,
194
- `${h.allocation.toFixed(1)}%`
195
- ]);
196
- }
197
- const pnlSign = summary.totalPnl >= 0 ? "+" : "";
198
- portfolioTable.setData({
199
- headers,
200
- data: rows
201
- });
202
- portfolioTable.options.label = ` PORTFOLIO Total: $${summary.totalValue.toFixed(2)} \u2502 P&L: ${pnlSign}$${Math.abs(summary.totalPnl).toFixed(2)} (${pnlSign}${summary.totalPnlPct.toFixed(2)}%) `;
203
- } catch {
204
- }
205
- screen.render();
206
- }
207
- function fmtTablePrice(p) {
208
- if (p >= 1e4) return `$${p.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
209
- if (p >= 100) return `$${p.toFixed(2)}`;
210
- if (p >= 1) return `$${p.toFixed(4)}`;
211
- return `$${p.toFixed(6)}`;
212
- }
213
- function fmtVolume(v) {
214
- if (v >= 1e9) return `$${(v / 1e9).toFixed(2)}B`;
215
- if (v >= 1e6) return `$${(v / 1e6).toFixed(1)}M`;
216
- if (v >= 1e3) return `$${(v / 1e3).toFixed(1)}K`;
217
- return `$${v.toFixed(2)}`;
218
- }
219
- function fmtAmount(a) {
220
- if (a >= 1e3) return a.toFixed(2);
221
- if (a >= 1) return a.toFixed(4);
222
- if (a >= 1e-3) return a.toFixed(6);
223
- return a.toFixed(8);
224
- }
225
- renderStatus();
226
- screen.render();
227
- priceTable.setData({ headers: ["Symbol", "Price", "24h %", "Volume"], data: [["Loading...", "", "", ""]] });
228
- portfolioTable.setData({ headers: ["Asset", "Amount", "Price", "Value", "Avg Buy", "P&L", "Alloc %"], data: [["Loading...", "", "", "", "", "", ""]] });
229
- screen.render();
230
- await Promise.all([refreshPrices(), refreshChart(), refreshPortfolio()]);
231
- const priceTimer = setInterval(async () => {
232
- await refreshPrices();
233
- }, cfg.refreshMs);
234
- const chartTimer = setInterval(async () => {
235
- await refreshChart();
236
- }, cfg.refreshMs * 3);
237
- const portfolioTimer = setInterval(async () => {
238
- await refreshPortfolio();
239
- }, cfg.refreshMs * 2);
240
- screen.on("destroy", () => {
241
- clearInterval(priceTimer);
242
- clearInterval(chartTimer);
243
- clearInterval(portfolioTimer);
244
- });
245
- priceTable.focus();
246
- screen.render();
247
- }
248
- export {
249
- startDashboard
250
- };