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 +114 -44
- package/dist/{core-UZMUAQBA.js → core-IDBWLFHU.js} +103 -2
- package/dist/dashboard-LP5MRI2P.js +480 -0
- package/dist/index.js +14 -14
- package/package.json +1 -1
- package/dist/dashboard-6GJ4UVRY.js +0 -250
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
|
-
|
|
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
|
-
\
|
|
35
|
-
\u2551
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
\
|
|
43
|
-
|
|
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
|
|
434
|
-
const secret = await
|
|
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
|
|
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-
|
|
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.
|
|
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
|
|
976
|
-
${c.cyan}4.${c.reset}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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
|
|
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
|
|
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-
|
|
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:
|
|
1314
|
-
const { homedir:
|
|
1315
|
-
const { join:
|
|
1316
|
-
const walletPath =
|
|
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
|
|
1325
|
-
|
|
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
|
-
|
|
1328
|
-
|
|
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}
|
|
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(`
|
|
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(`
|
|
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
|
-
|
|
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
|
-
\
|
|
3024
|
-
\u2551
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
\
|
|
3032
|
-
|
|
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,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
|
-
};
|