omnitrade-mcp 0.9.4 → 0.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -10
- package/dist/cli.js +104 -31
- package/dist/{core-UZMUAQBA.js → core-IDBWLFHU.js} +103 -2
- package/dist/dashboard-LP5MRI2P.js +480 -0
- package/dist/index.js +4 -1
- package/package.json +1 -1
- package/dist/dashboard-6GJ4UVRY.js +0 -250
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<br />
|
|
3
3
|
<picture>
|
|
4
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Connectry-io/omnitrade-mcp/main/.github/assets/logo-dark.svg">
|
|
5
|
-
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Connectry-io/omnitrade-mcp/main/.github/assets/logo-light.svg">
|
|
6
|
-
<img alt="OmniTrade" src="https://raw.githubusercontent.com/Connectry-io/omnitrade-mcp/main/.github/assets/logo-dark.svg" width="350">
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Connectry-io/connectrylab-omnitrade-mcp/main/.github/assets/logo-dark.svg">
|
|
5
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Connectry-io/connectrylab-omnitrade-mcp/main/.github/assets/logo-light.svg">
|
|
6
|
+
<img alt="OmniTrade" src="https://raw.githubusercontent.com/Connectry-io/connectrylab-omnitrade-mcp/main/.github/assets/logo-dark.svg" width="350">
|
|
7
7
|
</picture>
|
|
8
8
|
<br />
|
|
9
9
|
</p>
|
|
@@ -20,13 +20,14 @@
|
|
|
20
20
|
<p align="center">
|
|
21
21
|
<a href="https://www.npmjs.com/package/omnitrade-mcp"><img src="https://img.shields.io/npm/v/omnitrade-mcp?style=flat&colorA=18181B&colorB=28CF8D" alt="npm version"></a>
|
|
22
22
|
<a href="https://www.npmjs.com/package/omnitrade-mcp"><img src="https://img.shields.io/npm/dm/omnitrade-mcp?style=flat&colorA=18181B&colorB=28CF8D" alt="npm downloads"></a>
|
|
23
|
-
<a href="https://github.com/Connectry-io/omnitrade-mcp"><img src="https://img.shields.io/github/stars/Connectry-io/omnitrade-mcp?style=flat&colorA=18181B&colorB=28CF8D" alt="GitHub stars"></a>
|
|
24
|
-
<a href="https://github.com/Connectry-io/omnitrade-mcp/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Connectry-io/omnitrade-mcp?style=flat&colorA=18181B&colorB=28CF8D" alt="License"></a>
|
|
23
|
+
<a href="https://github.com/Connectry-io/connectrylab-omnitrade-mcp"><img src="https://img.shields.io/github/stars/Connectry-io/connectrylab-omnitrade-mcp?style=flat&colorA=18181B&colorB=28CF8D" alt="GitHub stars"></a>
|
|
24
|
+
<a href="https://github.com/Connectry-io/connectrylab-omnitrade-mcp/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Connectry-io/connectrylab-omnitrade-mcp?style=flat&colorA=18181B&colorB=28CF8D" alt="License"></a>
|
|
25
25
|
</p>
|
|
26
26
|
|
|
27
27
|
<p align="center">
|
|
28
28
|
<a href="#-quick-start">Quick Start</a> •
|
|
29
29
|
<a href="#-features">Features</a> •
|
|
30
|
+
<a href="#-desktop-app">Desktop App</a> •
|
|
30
31
|
<a href="#-examples">Examples</a> •
|
|
31
32
|
<a href="#-tui-dashboard">Dashboard</a> •
|
|
32
33
|
<a href="#-paper-trading">Paper Trading</a> •
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
|
|
37
38
|
---
|
|
38
39
|
|
|
39
|
-
> ⚠️ **Disclaimer:** OmniTrade is a developer tool and does not constitute financial advice. Cryptocurrency trading involves substantial risk. Connectry Labs is not a licensed financial advisor. Always do your own research and consult a qualified financial advisor before making investment decisions. [Use at your own risk.](https://github.com/Connectry-io/omnitrade-mcp/blob/main/LICENSE)
|
|
40
|
+
> ⚠️ **Disclaimer:** OmniTrade is a developer tool and does not constitute financial advice. Cryptocurrency trading involves substantial risk. Connectry Labs is not a licensed financial advisor. Always do your own research and consult a qualified financial advisor before making investment decisions. [Use at your own risk.](https://github.com/Connectry-io/connectrylab-omnitrade-mcp/blob/main/LICENSE)
|
|
40
41
|
|
|
41
42
|
## 💬 What is OmniTrade?
|
|
42
43
|
|
|
@@ -119,6 +120,26 @@ Practice risk-free with a virtual $10,000 USDT wallet. Buy and sell at live mark
|
|
|
119
120
|
|
|
120
121
|
<br />
|
|
121
122
|
|
|
123
|
+
## 🖥️ Desktop App
|
|
124
|
+
|
|
125
|
+
OmniTrade ships a native desktop GUI built with Tauri — Dashboard, Prices, Portfolio, Alerts, DCA, and Settings in a standalone app.
|
|
126
|
+
|
|
127
|
+
### Download
|
|
128
|
+
|
|
129
|
+
| Platform | File | Notes |
|
|
130
|
+
|----------|------|-------|
|
|
131
|
+
| macOS | `.dmg` | Built automatically on each release |
|
|
132
|
+
| Windows | `.msi` / `.exe` | Built automatically on each release |
|
|
133
|
+
| Linux (portable) | `.AppImage` | Run without installing |
|
|
134
|
+
| Linux (Debian/Ubuntu) | `.deb` | `dpkg -i OmniTrade_*.deb` |
|
|
135
|
+
| Linux (RedHat/Fedora) | `.rpm` | `rpm -i OmniTrade-*.rpm` |
|
|
136
|
+
|
|
137
|
+
👉 **[Download from GitHub Releases](https://github.com/Connectry-io/connectrylab-omnitrade-mcp/releases)**
|
|
138
|
+
|
|
139
|
+
The desktop app reads your existing `~/.omnitrade/config.json` — no extra setup needed after running `omnitrade setup`.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
122
143
|
## 🚀 Quick Start
|
|
123
144
|
|
|
124
145
|
### 1. Install
|
|
@@ -499,13 +520,17 @@ The wizard walks you through:
|
|
|
499
520
|
| Channel | How to configure |
|
|
500
521
|
|---------|-----------------|
|
|
501
522
|
| **Native OS** | Zero setup — uses system notifications on macOS, Windows, and Linux |
|
|
502
|
-
| **Telegram** | Create a bot via [@BotFather](https://t.me/BotFather), get your
|
|
523
|
+
| **Telegram** | Create a bot via [@BotFather](https://t.me/BotFather) → get the **Bot Token** (long string like `7481234567:AAHdqTcvCH1v...`), then get your **Chat ID** (numeric, from the getUpdates URL) |
|
|
503
524
|
| **Discord** | Create a webhook in your server's channel settings, paste the URL |
|
|
504
525
|
|
|
526
|
+
> **API key input:** Keys and secrets are masked as you type — nothing shown in plaintext in the terminal.
|
|
527
|
+
|
|
505
528
|
4. **Claude integration** — auto-writes to Claude Desktop config and optionally `~/.claude/settings.json` for Claude Code
|
|
506
529
|
|
|
507
530
|
You can re-run `omnitrade setup` at any time to update credentials or add new notification channels.
|
|
508
531
|
|
|
532
|
+
> **Tip:** Re-running setup won't wipe your existing config. It detects your current keys and lets you keep, update, or skip each exchange individually.
|
|
533
|
+
|
|
509
534
|
### Dashboard — Full-Screen TUI *(v0.9.0)*
|
|
510
535
|
|
|
511
536
|
Launch the Bloomberg Terminal-style interface with live prices, charts, and portfolio panel.
|
|
@@ -677,7 +702,7 @@ OmniTrade provides **40 tools** organized by category:
|
|
|
677
702
|
- ✅ **API keys stay on your machine** — Never sent anywhere else
|
|
678
703
|
- ✅ **No cloud storage** — Everything local
|
|
679
704
|
- ✅ **No telemetry** — Zero data collection
|
|
680
|
-
- ✅ **Open source** — [Audit the code yourself](https://github.com/Connectry-io/omnitrade-mcp)
|
|
705
|
+
- ✅ **Open source** — [Audit the code yourself](https://github.com/Connectry-io/connectrylab-omnitrade-mcp)
|
|
681
706
|
|
|
682
707
|
### API Key Best Practices
|
|
683
708
|
|
|
@@ -741,7 +766,7 @@ We love contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
|
741
766
|
|
|
742
767
|
```bash
|
|
743
768
|
# Clone the repo
|
|
744
|
-
git clone https://github.com/Connectry-io/omnitrade-mcp.git
|
|
769
|
+
git clone https://github.com/Connectry-io/connectrylab-omnitrade-mcp.git
|
|
745
770
|
cd omnitrade-mcp
|
|
746
771
|
|
|
747
772
|
# Install dependencies
|
|
@@ -791,7 +816,7 @@ OmniTrade is a project by [Connectry Labs](https://connectry.io/labs) — the in
|
|
|
791
816
|
</p>
|
|
792
817
|
|
|
793
818
|
<p align="center">
|
|
794
|
-
<a href="https://github.com/Connectry-io/omnitrade-mcp">GitHub</a> •
|
|
819
|
+
<a href="https://github.com/Connectry-io/connectrylab-omnitrade-mcp">GitHub</a> •
|
|
795
820
|
<a href="https://www.npmjs.com/package/omnitrade-mcp">npm</a> •
|
|
796
821
|
<a href="https://connectry.io/labs/omnitrade">Website</a>
|
|
797
822
|
</p>
|
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,10 @@ import { homedir } from "os";
|
|
|
13
13
|
import { join } from "path";
|
|
14
14
|
import * as readline from "readline";
|
|
15
15
|
import { spawn } from "child_process";
|
|
16
|
-
|
|
16
|
+
import { createRequire } from "module";
|
|
17
|
+
var _require = createRequire(import.meta.url);
|
|
18
|
+
var _pkg = _require("../package.json");
|
|
19
|
+
var VERSION = _pkg.version;
|
|
17
20
|
var CONFIG_PATH = join(homedir(), ".omnitrade", "config.json");
|
|
18
21
|
var c = {
|
|
19
22
|
reset: "\x1B[0m",
|
|
@@ -29,6 +32,36 @@ var c = {
|
|
|
29
32
|
orange: "\x1B[38;5;208m",
|
|
30
33
|
red: "\x1B[38;5;196m"
|
|
31
34
|
};
|
|
35
|
+
async function maskedQuestion(prompt) {
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
process.stdout.write(prompt);
|
|
38
|
+
let input = "";
|
|
39
|
+
process.stdin.setRawMode(true);
|
|
40
|
+
process.stdin.resume();
|
|
41
|
+
process.stdin.setEncoding("utf8");
|
|
42
|
+
process.stdin.on("data", function handler(char) {
|
|
43
|
+
if (char === "\r" || char === "\n") {
|
|
44
|
+
process.stdin.setRawMode(false);
|
|
45
|
+
process.stdin.pause();
|
|
46
|
+
process.stdin.removeListener("data", handler);
|
|
47
|
+
process.stdout.write("\n");
|
|
48
|
+
resolve(input);
|
|
49
|
+
} else if (char === "") {
|
|
50
|
+
process.exit();
|
|
51
|
+
} else if (char === "\x7F") {
|
|
52
|
+
if (input.length > 0) {
|
|
53
|
+
input = input.slice(0, -1);
|
|
54
|
+
process.stdout.clearLine(0);
|
|
55
|
+
process.stdout.cursorTo(0);
|
|
56
|
+
process.stdout.write(prompt + "*".repeat(input.length));
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
input += char;
|
|
60
|
+
process.stdout.write("*");
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
32
65
|
function printBanner() {
|
|
33
66
|
console.log(`
|
|
34
67
|
${c.purple}\u2588\u2588\u2588\u2588\u2588\u2588\u2557 ${c.reset}\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
@@ -388,6 +421,15 @@ async function runSetupWizard() {
|
|
|
388
421
|
console.log(`
|
|
389
422
|
${c.green}\u2713${c.reset} Selected: ${selectedExchanges.map((e) => e.info?.name || e.id).join(", ")}
|
|
390
423
|
`);
|
|
424
|
+
let existingConfigForSkip = {};
|
|
425
|
+
if (existsSync(CONFIG_PATH)) {
|
|
426
|
+
try {
|
|
427
|
+
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
|
428
|
+
existingConfigForSkip = JSON.parse(raw);
|
|
429
|
+
} catch {
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const existingExchanges = existingConfigForSkip.exchanges || {};
|
|
391
433
|
const config = {
|
|
392
434
|
exchanges: {},
|
|
393
435
|
security: {
|
|
@@ -404,6 +446,17 @@ async function runSetupWizard() {
|
|
|
404
446
|
${c.white}${c.bold}STEP ${stepNum}/${totalSteps} \u2014 ${displayName} API KEYS${c.reset}
|
|
405
447
|
${c.gray}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${c.reset}
|
|
406
448
|
`);
|
|
449
|
+
const existing = existingExchanges[exchange];
|
|
450
|
+
if (existing?.apiKey?.trim() && existing?.secret?.trim()) {
|
|
451
|
+
const maskedKey = `${existing.apiKey.slice(0, 5)}...${existing.apiKey.slice(-5)}`;
|
|
452
|
+
console.log(` ${c.green}\u2713${c.reset} ${displayName} already configured ${c.dim}(apiKey: ${maskedKey})${c.reset}`);
|
|
453
|
+
const keepAnswer = await question(` ${c.yellow}?${c.reset} Keep existing keys? ${c.dim}(Y/n)${c.reset}: `);
|
|
454
|
+
if (keepAnswer.toLowerCase() !== "n") {
|
|
455
|
+
config.exchanges[exchange] = existing;
|
|
456
|
+
console.log(` ${c.green}\u2713${c.reset} ${displayName} kept (existing keys)`);
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
407
460
|
if (exchangeInfo) {
|
|
408
461
|
console.log(` ${c.dim}Create API keys at:${c.reset} ${c.blue}${exchangeInfo.apiUrl}${c.reset}
|
|
409
462
|
`);
|
|
@@ -427,12 +480,12 @@ async function runSetupWizard() {
|
|
|
427
480
|
console.log(`
|
|
428
481
|
${c.dim}Paste your ${displayName} credentials:${c.reset}
|
|
429
482
|
`);
|
|
430
|
-
const apiKey = await
|
|
431
|
-
const secret = await
|
|
483
|
+
const apiKey = await maskedQuestion(` ${c.cyan}API Key:${c.reset} `);
|
|
484
|
+
const secret = await maskedQuestion(` ${c.cyan}Secret:${c.reset} `);
|
|
432
485
|
let password = "";
|
|
433
486
|
const needsPassphrase = exchangeInfo?.needsPassphrase || ["coinbase", "kucoin", "okx", "bitget"].includes(exchange);
|
|
434
487
|
if (needsPassphrase) {
|
|
435
|
-
password = await
|
|
488
|
+
password = await maskedQuestion(` ${c.cyan}Passphrase:${c.reset} `);
|
|
436
489
|
}
|
|
437
490
|
let testnet = false;
|
|
438
491
|
const hasTestnet = exchangeInfo?.testnetUrl || ["binance", "bybit"].includes(exchange);
|
|
@@ -823,7 +876,7 @@ async function daemonStatus() {
|
|
|
823
876
|
console.log("");
|
|
824
877
|
}
|
|
825
878
|
async function daemonRun() {
|
|
826
|
-
const { startDaemon } = await import("./core-
|
|
879
|
+
const { startDaemon } = await import("./core-IDBWLFHU.js");
|
|
827
880
|
await startDaemon();
|
|
828
881
|
}
|
|
829
882
|
async function watchPrices(symbols) {
|
|
@@ -967,16 +1020,25 @@ async function setupNotifications(question) {
|
|
|
967
1020
|
${c.white}${c.bold}TELEGRAM SETUP${c.reset}
|
|
968
1021
|
${c.gray}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${c.reset}
|
|
969
1022
|
|
|
970
|
-
${c.
|
|
1023
|
+
${c.white}${c.bold}STEP 1 \u2014 CREATE YOUR BOT${c.reset}
|
|
1024
|
+
${c.cyan}1.${c.reset} Open Telegram \u2192 search ${c.white}@BotFather${c.reset} \u2192 start a chat
|
|
971
1025
|
${c.cyan}2.${c.reset} Send: ${c.white}/newbot${c.reset}
|
|
972
|
-
${c.cyan}3.${c.reset} Follow prompts
|
|
973
|
-
${c.cyan}4.${c.reset}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1026
|
+
${c.cyan}3.${c.reset} Follow prompts (choose a name and username for your bot)
|
|
1027
|
+
${c.cyan}4.${c.reset} BotFather gives you a ${c.white}Bot Token${c.reset} \u2014 it looks like this:
|
|
1028
|
+
${c.dim}7481234567:AAHdqTcvCH1vGWJxfSeofSH2Y34H4ouyJe4${c.reset}
|
|
1029
|
+
Copy it.
|
|
1030
|
+
|
|
1031
|
+
${c.white}${c.bold}STEP 2 \u2014 GET YOUR CHAT ID${c.reset}
|
|
1032
|
+
${c.cyan}1.${c.reset} In Telegram, search for ${c.white}YOUR BOT${c.reset} by its @username
|
|
1033
|
+
${c.cyan}2.${c.reset} Open the chat with it
|
|
1034
|
+
${c.cyan}3.${c.reset} Send any message (type "hi" and hit send)
|
|
1035
|
+
${c.cyan}4.${c.reset} Visit this URL in your browser (replace TOKEN with yours):
|
|
1036
|
+
${c.blue}https://api.telegram.org/bot<TOKEN>/getUpdates${c.reset}
|
|
1037
|
+
${c.cyan}5.${c.reset} Look for ${c.white}"chat": {"id": 1554736939 ...}${c.reset}
|
|
1038
|
+
That number is your ${c.white}Chat ID${c.reset}.
|
|
977
1039
|
|
|
978
1040
|
`);
|
|
979
|
-
const botToken = await
|
|
1041
|
+
const botToken = await maskedQuestion(` ${c.cyan}Bot token:${c.reset} `);
|
|
980
1042
|
const chatId = await question(` ${c.cyan}Chat ID:${c.reset} `);
|
|
981
1043
|
if (botToken.trim() && chatId.trim()) {
|
|
982
1044
|
process.stdout.write(` Verifying... `);
|
|
@@ -1015,7 +1077,7 @@ async function setupNotifications(question) {
|
|
|
1015
1077
|
${c.cyan}3.${c.reset} Name it "OmniTrade" and copy the Webhook URL
|
|
1016
1078
|
|
|
1017
1079
|
`);
|
|
1018
|
-
const webhookUrl = await
|
|
1080
|
+
const webhookUrl = await maskedQuestion(` ${c.cyan}Webhook URL:${c.reset} `);
|
|
1019
1081
|
if (webhookUrl.trim()) {
|
|
1020
1082
|
process.stdout.write(` Verifying... `);
|
|
1021
1083
|
try {
|
|
@@ -1131,16 +1193,29 @@ main().catch((error) => {
|
|
|
1131
1193
|
async function runDashboard(args) {
|
|
1132
1194
|
const symbolIdx = args.indexOf("--symbol");
|
|
1133
1195
|
const chartSymbol = symbolIdx !== -1 ? (args[symbolIdx + 1] ?? "BTC").toUpperCase() : "BTC";
|
|
1196
|
+
const symbolsIdx = args.indexOf("--symbols");
|
|
1197
|
+
let customSymbols;
|
|
1198
|
+
if (symbolsIdx !== -1) {
|
|
1199
|
+
const symbolArgs = [];
|
|
1200
|
+
for (let i = symbolsIdx + 1; i < args.length; i++) {
|
|
1201
|
+
if (args[i].startsWith("--")) break;
|
|
1202
|
+
symbolArgs.push(args[i].toUpperCase());
|
|
1203
|
+
}
|
|
1204
|
+
if (symbolArgs.length > 0) customSymbols = symbolArgs;
|
|
1205
|
+
}
|
|
1134
1206
|
const refreshIdx = args.indexOf("--refresh");
|
|
1135
1207
|
const refreshSec = refreshIdx !== -1 ? parseInt(args[refreshIdx + 1] ?? "8", 10) : 8;
|
|
1208
|
+
const live = args.includes("--live");
|
|
1136
1209
|
console.log(`${c.cyan}Starting OmniTrade Dashboard...${c.reset}`);
|
|
1137
|
-
console.log(`${c.dim}Chart: ${chartSymbol}/USDT \u2502 Refresh: ${refreshSec}s \u2502 Press q to quit${c.reset}
|
|
1210
|
+
console.log(`${c.dim}Chart: ${chartSymbol}/USDT \u2502 Refresh: ${refreshSec}s \u2502 Mode: ${live ? "LIVE" : "paper"} \u2502 Press q to quit${c.reset}
|
|
1138
1211
|
`);
|
|
1139
1212
|
try {
|
|
1140
|
-
const { startDashboard } = await import("./dashboard-
|
|
1213
|
+
const { startDashboard } = await import("./dashboard-LP5MRI2P.js");
|
|
1141
1214
|
await startDashboard({
|
|
1142
1215
|
chartSymbol,
|
|
1143
|
-
refreshMs: refreshSec * 1e3
|
|
1216
|
+
refreshMs: refreshSec * 1e3,
|
|
1217
|
+
live,
|
|
1218
|
+
...customSymbols ? { symbols: customSymbols } : {}
|
|
1144
1219
|
});
|
|
1145
1220
|
} catch (err) {
|
|
1146
1221
|
console.error(`${c.red}Dashboard error:${c.reset}`, err.message);
|
|
@@ -1307,28 +1382,26 @@ ${c.red}\u2717 History error:${c.reset} ${err.message}
|
|
|
1307
1382
|
}
|
|
1308
1383
|
// ── paper reset ───────────────────────────────────────────
|
|
1309
1384
|
case "reset": {
|
|
1310
|
-
const { existsSync:
|
|
1311
|
-
const { homedir:
|
|
1312
|
-
const { join:
|
|
1313
|
-
const walletPath =
|
|
1314
|
-
if (!existsSync2(walletPath)) {
|
|
1315
|
-
console.log(`
|
|
1316
|
-
${c.yellow}\u26A0${c.reset} No paper wallet found.
|
|
1317
|
-
`);
|
|
1318
|
-
return;
|
|
1319
|
-
}
|
|
1385
|
+
const { existsSync: fsExists, unlinkSync } = await import("fs");
|
|
1386
|
+
const { homedir: hd } = await import("os");
|
|
1387
|
+
const { join: pjoin } = await import("path");
|
|
1388
|
+
const walletPath = pjoin(hd(), ".omnitrade", "paper-wallet.json");
|
|
1320
1389
|
const rl = (await import("readline")).createInterface({ input: process.stdin, output: process.stdout });
|
|
1321
|
-
const
|
|
1322
|
-
|
|
1390
|
+
const walletExists = fsExists(walletPath);
|
|
1391
|
+
const promptMsg = walletExists ? `
|
|
1392
|
+
${c.yellow}\u26A0 Reset paper wallet? This clears all trades and restarts with $10,000 (y/N): ${c.reset}` : `
|
|
1393
|
+
${c.yellow}? No paper wallet found. Create a fresh $10,000 wallet? (Y/n): ${c.reset}`;
|
|
1394
|
+
const answer = await new Promise((r) => rl.question(promptMsg, r));
|
|
1323
1395
|
rl.close();
|
|
1324
|
-
|
|
1325
|
-
|
|
1396
|
+
const confirmed = walletExists ? answer.toLowerCase() === "y" : answer.toLowerCase() !== "n";
|
|
1397
|
+
if (confirmed) {
|
|
1398
|
+
if (walletExists) unlinkSync(walletPath);
|
|
1326
1399
|
loadWallet();
|
|
1327
1400
|
console.log(`
|
|
1328
1401
|
${c.green}\u2713 Paper wallet reset to $10,000 USDT${c.reset}
|
|
1329
1402
|
`);
|
|
1330
1403
|
} else {
|
|
1331
|
-
console.log(`${c.dim}
|
|
1404
|
+
console.log(`${c.dim}Cancelled.${c.reset}
|
|
1332
1405
|
`);
|
|
1333
1406
|
}
|
|
1334
1407
|
break;
|
|
@@ -112,6 +112,7 @@ async function sendNotification(config, title, message) {
|
|
|
112
112
|
// src/daemon/core.ts
|
|
113
113
|
var OMNITRADE_DIR = join(homedir(), ".omnitrade");
|
|
114
114
|
var ALERTS_FILE = join(OMNITRADE_DIR, "alerts.json");
|
|
115
|
+
var DCA_FILE = join(OMNITRADE_DIR, "dca.json");
|
|
115
116
|
var LOG_FILE = join(OMNITRADE_DIR, "daemon.log");
|
|
116
117
|
function log(message) {
|
|
117
118
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -140,6 +141,96 @@ async function saveAlerts(data) {
|
|
|
140
141
|
await fs.mkdir(OMNITRADE_DIR, { recursive: true });
|
|
141
142
|
await fs.writeFile(ALERTS_FILE, JSON.stringify(data, null, 2));
|
|
142
143
|
}
|
|
144
|
+
async function loadDCAConfigs() {
|
|
145
|
+
if (!existsSync(DCA_FILE)) {
|
|
146
|
+
return { configs: [] };
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const raw = await fs.readFile(DCA_FILE, "utf-8");
|
|
150
|
+
return JSON.parse(raw);
|
|
151
|
+
} catch {
|
|
152
|
+
return { configs: [] };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function saveDCAConfigs(data) {
|
|
156
|
+
await fs.mkdir(OMNITRADE_DIR, { recursive: true });
|
|
157
|
+
await fs.writeFile(DCA_FILE, JSON.stringify(data, null, 2));
|
|
158
|
+
}
|
|
159
|
+
function getDCAFrequencyMs(frequency) {
|
|
160
|
+
const intervals = {
|
|
161
|
+
hourly: 60 * 60 * 1e3,
|
|
162
|
+
daily: 24 * 60 * 60 * 1e3,
|
|
163
|
+
weekly: 7 * 24 * 60 * 60 * 1e3,
|
|
164
|
+
monthly: 30 * 24 * 60 * 60 * 1e3
|
|
165
|
+
};
|
|
166
|
+
return intervals[frequency];
|
|
167
|
+
}
|
|
168
|
+
function isDCADue(dca, now) {
|
|
169
|
+
if (!dca.enabled) return false;
|
|
170
|
+
if (!dca.lastExecuted) return true;
|
|
171
|
+
return now - dca.lastExecuted >= getDCAFrequencyMs(dca.frequency);
|
|
172
|
+
}
|
|
173
|
+
async function pollAndCheckDCAs(exchanges, config) {
|
|
174
|
+
const data = await loadDCAConfigs();
|
|
175
|
+
const now = Date.now();
|
|
176
|
+
const dueDCAs = data.configs.filter((d) => isDCADue(d, now));
|
|
177
|
+
if (dueDCAs.length === 0) {
|
|
178
|
+
log(`DCA check \u2014 no orders due`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
log(`DCA check \u2014 ${dueDCAs.length} order(s) due`);
|
|
182
|
+
for (const dca of dueDCAs) {
|
|
183
|
+
const exchange = exchanges.get(dca.exchange);
|
|
184
|
+
if (!exchange) {
|
|
185
|
+
log(` \u26A0 DCA ${dca.id}: exchange ${dca.exchange} not available \u2014 skipping`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const ticker = await exchange.fetchTicker(dca.symbol);
|
|
190
|
+
const price = ticker.last ?? 0;
|
|
191
|
+
if (price <= 0) {
|
|
192
|
+
log(` \u26A0 DCA ${dca.id}: invalid price for ${dca.symbol} \u2014 skipping`);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const exchCfg = config.exchanges[dca.exchange];
|
|
196
|
+
const hasCredentials = !!(exchCfg?.apiKey && exchCfg?.secret);
|
|
197
|
+
let spent = dca.amountUSD;
|
|
198
|
+
if (hasCredentials) {
|
|
199
|
+
try {
|
|
200
|
+
const amount = dca.amountUSD / price;
|
|
201
|
+
const order = await exchange.createMarketBuyOrder(dca.symbol, amount);
|
|
202
|
+
spent = order.cost ?? dca.amountUSD;
|
|
203
|
+
log(` \u2713 DCA ${dca.id}: REAL buy ${dca.symbol} \u2014 $${spent.toFixed(2)} at $${price.toFixed(2)} (order: ${order.id})`);
|
|
204
|
+
} catch (orderErr) {
|
|
205
|
+
log(` \u26A0 DCA ${dca.id}: real order failed, logging as simulated: ${orderErr.message}`);
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
log(` \u2713 DCA ${dca.id}: SIMULATED buy ${dca.symbol} \u2014 $${dca.amountUSD.toFixed(2)} at $${price.toFixed(2)} [no credentials]`);
|
|
209
|
+
}
|
|
210
|
+
dca.lastExecuted = now;
|
|
211
|
+
dca.totalExecutions += 1;
|
|
212
|
+
dca.totalSpent += spent;
|
|
213
|
+
const baseAsset = dca.symbol.split("/")[0] ?? dca.symbol;
|
|
214
|
+
const title = `OmniTrade DCA: ${baseAsset}`;
|
|
215
|
+
const message = `DCA executed: bought $${dca.amountUSD} of ${baseAsset} at $${price.toFixed(2)} on ${dca.exchange}`;
|
|
216
|
+
const results = await sendNotification(config.notifications, title, message);
|
|
217
|
+
for (const result of results) {
|
|
218
|
+
if (result.success) {
|
|
219
|
+
log(` \u2713 DCA notification sent via ${result.channel}`);
|
|
220
|
+
} else {
|
|
221
|
+
log(` \u2717 DCA notification failed via ${result.channel}: ${result.error}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (results.length === 0) {
|
|
225
|
+
log(` \u2139 DCA: no notification channels configured`);
|
|
226
|
+
}
|
|
227
|
+
} catch (err) {
|
|
228
|
+
log(` \u2717 DCA ${dca.id}: error \u2014 ${err.message}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
await saveDCAConfigs(data);
|
|
232
|
+
log(`DCA check complete \u2014 ${dueDCAs.length} processed`);
|
|
233
|
+
}
|
|
143
234
|
function createPublicExchange(name) {
|
|
144
235
|
const id = name.toLowerCase();
|
|
145
236
|
if (!ccxt.exchanges.includes(id)) return null;
|
|
@@ -244,13 +335,23 @@ async function startDaemon() {
|
|
|
244
335
|
try {
|
|
245
336
|
await pollAndCheckAlerts(exchanges, config);
|
|
246
337
|
} catch (err) {
|
|
247
|
-
log(`
|
|
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,7 +3015,10 @@ 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";
|
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
|
-
};
|