omnitrade-mcp 0.9.3 → 0.9.5

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/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.3";
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,21 +32,48 @@ 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
- \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
35
- \u2551 \u2551
36
- \u2551 ${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 \u2551
37
- \u2551 ${c.purple}\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557${c.reset}\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2551
38
- \u2551 ${c.purple}\u2588\u2588\u2551 \u2588\u2588\u2551${c.reset}\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2551
39
- \u2551 ${c.purple}\u2588\u2588\u2551 \u2588\u2588\u2551${c.reset}\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2551
40
- \u2551 ${c.purple}\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D${c.reset}\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2551
41
- \u2551 ${c.purple}\u255A\u2550\u2550\u2550\u2550\u2550\u255D ${c.reset}\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u2551
42
- \u2551 \u2551
43
- \u2551 v${VERSION} \u2022 One AI. 107 Exchanges. Natural language trading. \u2551
44
- \u2551 by Connectry Labs \u2022 https://connectry.io \u2551
45
- \u2551 \u2551
46
- \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
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
68
+ ${c.purple}\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557${c.reset}\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D
69
+ ${c.purple}\u2588\u2588\u2551 \u2588\u2588\u2551${c.reset}\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557
70
+ ${c.purple}\u2588\u2588\u2551 \u2588\u2588\u2551${c.reset}\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D
71
+ ${c.purple}\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D${c.reset}\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
72
+ ${c.purple}\u255A\u2550\u2550\u2550\u2550\u2550\u255D ${c.reset}\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D
73
+
74
+ ${c.gray}v${VERSION} \u2022 One AI. 107 Exchanges. Natural language trading.${c.reset}
75
+ ${c.gray}by Connectry Labs \u2022 https://connectry.io${c.reset}
76
+ ${c.gray}${"\u2500".repeat(75)}${c.reset}
47
77
  `);
48
78
  }
49
79
  function printCompactLogo() {
@@ -391,6 +421,15 @@ async function runSetupWizard() {
391
421
  console.log(`
392
422
  ${c.green}\u2713${c.reset} Selected: ${selectedExchanges.map((e) => e.info?.name || e.id).join(", ")}
393
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 || {};
394
433
  const config = {
395
434
  exchanges: {},
396
435
  security: {
@@ -407,6 +446,17 @@ async function runSetupWizard() {
407
446
  ${c.white}${c.bold}STEP ${stepNum}/${totalSteps} \u2014 ${displayName} API KEYS${c.reset}
408
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}
409
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
+ }
410
460
  if (exchangeInfo) {
411
461
  console.log(` ${c.dim}Create API keys at:${c.reset} ${c.blue}${exchangeInfo.apiUrl}${c.reset}
412
462
  `);
@@ -430,12 +480,12 @@ async function runSetupWizard() {
430
480
  console.log(`
431
481
  ${c.dim}Paste your ${displayName} credentials:${c.reset}
432
482
  `);
433
- const apiKey = await question(` ${c.cyan}API Key:${c.reset} `);
434
- 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} `);
435
485
  let password = "";
436
486
  const needsPassphrase = exchangeInfo?.needsPassphrase || ["coinbase", "kucoin", "okx", "bitget"].includes(exchange);
437
487
  if (needsPassphrase) {
438
- password = await question(` ${c.cyan}Passphrase:${c.reset} `);
488
+ password = await maskedQuestion(` ${c.cyan}Passphrase:${c.reset} `);
439
489
  }
440
490
  let testnet = false;
441
491
  const hasTestnet = exchangeInfo?.testnetUrl || ["binance", "bybit"].includes(exchange);
@@ -826,7 +876,7 @@ async function daemonStatus() {
826
876
  console.log("");
827
877
  }
828
878
  async function daemonRun() {
829
- const { startDaemon } = await import("./core-UZMUAQBA.js");
879
+ const { startDaemon } = await import("./core-IDBWLFHU.js");
830
880
  await startDaemon();
831
881
  }
832
882
  async function watchPrices(symbols) {
@@ -970,16 +1020,25 @@ async function setupNotifications(question) {
970
1020
  ${c.white}${c.bold}TELEGRAM SETUP${c.reset}
971
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}
972
1022
 
973
- ${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
974
1025
  ${c.cyan}2.${c.reset} Send: ${c.white}/newbot${c.reset}
975
- ${c.cyan}3.${c.reset} Follow prompts to create your bot
976
- ${c.cyan}4.${c.reset} Copy the ${c.white}HTTP API token${c.reset} BotFather gives you
977
- ${c.cyan}5.${c.reset} Send ${c.white}/start${c.reset} to your new bot
978
- ${c.cyan}6.${c.reset} Visit: ${c.blue}https://api.telegram.org/bot<TOKEN>/getUpdates${c.reset}
979
- 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}.
980
1039
 
981
1040
  `);
982
- const botToken = await question(` ${c.cyan}Bot token:${c.reset} `);
1041
+ const botToken = await maskedQuestion(` ${c.cyan}Bot token:${c.reset} `);
983
1042
  const chatId = await question(` ${c.cyan}Chat ID:${c.reset} `);
984
1043
  if (botToken.trim() && chatId.trim()) {
985
1044
  process.stdout.write(` Verifying... `);
@@ -1018,7 +1077,7 @@ async function setupNotifications(question) {
1018
1077
  ${c.cyan}3.${c.reset} Name it "OmniTrade" and copy the Webhook URL
1019
1078
 
1020
1079
  `);
1021
- const webhookUrl = await question(` ${c.cyan}Webhook URL:${c.reset} `);
1080
+ const webhookUrl = await maskedQuestion(` ${c.cyan}Webhook URL:${c.reset} `);
1022
1081
  if (webhookUrl.trim()) {
1023
1082
  process.stdout.write(` Verifying... `);
1024
1083
  try {
@@ -1134,16 +1193,29 @@ main().catch((error) => {
1134
1193
  async function runDashboard(args) {
1135
1194
  const symbolIdx = args.indexOf("--symbol");
1136
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
+ }
1137
1206
  const refreshIdx = args.indexOf("--refresh");
1138
1207
  const refreshSec = refreshIdx !== -1 ? parseInt(args[refreshIdx + 1] ?? "8", 10) : 8;
1208
+ const live = args.includes("--live");
1139
1209
  console.log(`${c.cyan}Starting OmniTrade Dashboard...${c.reset}`);
1140
- 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}
1141
1211
  `);
1142
1212
  try {
1143
- const { startDashboard } = await import("./dashboard-6GJ4UVRY.js");
1213
+ const { startDashboard } = await import("./dashboard-LP5MRI2P.js");
1144
1214
  await startDashboard({
1145
1215
  chartSymbol,
1146
- refreshMs: refreshSec * 1e3
1216
+ refreshMs: refreshSec * 1e3,
1217
+ live,
1218
+ ...customSymbols ? { symbols: customSymbols } : {}
1147
1219
  });
1148
1220
  } catch (err) {
1149
1221
  console.error(`${c.red}Dashboard error:${c.reset}`, err.message);
@@ -1310,28 +1382,26 @@ ${c.red}\u2717 History error:${c.reset} ${err.message}
1310
1382
  }
1311
1383
  // ── paper reset ───────────────────────────────────────────
1312
1384
  case "reset": {
1313
- const { existsSync: existsSync2, unlinkSync } = await import("fs");
1314
- const { homedir: homedir2 } = await import("os");
1315
- const { join: join2 } = await import("path");
1316
- const walletPath = join2(homedir2(), ".omnitrade", "paper-wallet.json");
1317
- if (!existsSync2(walletPath)) {
1318
- console.log(`
1319
- ${c.yellow}\u26A0${c.reset} No paper wallet found.
1320
- `);
1321
- return;
1322
- }
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");
1323
1389
  const rl = (await import("readline")).createInterface({ input: process.stdin, output: process.stdout });
1324
- const answer = await new Promise((r) => rl.question(`
1325
- ${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));
1326
1395
  rl.close();
1327
- if (answer.toLowerCase() === "y") {
1328
- unlinkSync(walletPath);
1396
+ const confirmed = walletExists ? answer.toLowerCase() === "y" : answer.toLowerCase() !== "n";
1397
+ if (confirmed) {
1398
+ if (walletExists) unlinkSync(walletPath);
1329
1399
  loadWallet();
1330
1400
  console.log(`
1331
1401
  ${c.green}\u2713 Paper wallet reset to $10,000 USDT${c.reset}
1332
1402
  `);
1333
1403
  } else {
1334
- console.log(`${c.dim}Reset cancelled.${c.reset}
1404
+ console.log(`${c.dim}Cancelled.${c.reset}
1335
1405
  `);
1336
1406
  }
1337
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,24 +3015,24 @@ function registerConditionalOrderTools(server, exchangeManager, config) {
3015
3015
  }
3016
3016
 
3017
3017
  // src/index.ts
3018
- var VERSION = "0.9.3";
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";
3022
3025
  console.error(`
3023
- \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
3024
- \u2551 \u2551
3025
- \u2551 ${purple}\u2588\u2588\u2588\u2588\u2588\u2588\u2557 ${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 \u2551
3026
- \u2551 ${purple}\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557${reset}\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2551
3027
- \u2551 ${purple}\u2588\u2588\u2551 \u2588\u2588\u2551${reset}\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2551
3028
- \u2551 ${purple}\u2588\u2588\u2551 \u2588\u2588\u2551${reset}\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2551
3029
- \u2551 ${purple}\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D${reset}\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2551
3030
- \u2551 ${purple}\u255A\u2550\u2550\u2550\u2550\u2550\u255D ${reset}\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u2551
3031
- \u2551 \u2551
3032
- \u2551 MCP Server v${VERSION} \u2022 One AI. 107 Exchanges. Natural language trading. \u2551
3033
- \u2551 by Connectry Labs \u2022 https://connectry.io \u2551
3034
- \u2551 \u2551
3035
- \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
3026
+ ${purple}\u2588\u2588\u2588\u2588\u2588\u2588\u2557 ${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
3027
+ ${purple}\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557${reset}\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D
3028
+ ${purple}\u2588\u2588\u2551 \u2588\u2588\u2551${reset}\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557
3029
+ ${purple}\u2588\u2588\u2551 \u2588\u2588\u2551${reset}\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D
3030
+ ${purple}\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D${reset}\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
3031
+ ${purple}\u255A\u2550\u2550\u2550\u2550\u2550\u255D ${reset}\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D
3032
+
3033
+ MCP Server v${VERSION} \u2022 One AI. 107 Exchanges. Natural language trading.
3034
+ by Connectry Labs \u2022 https://connectry.io
3035
+ ${"\u2500".repeat(75)}
3036
3036
  `);
3037
3037
  }
3038
3038
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnitrade-mcp",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
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
- };