From 689658070b799963c855d2e754905fea29950920 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Mar 2026 10:31:56 +0000 Subject: [PATCH 1/2] Add multi-user Telegram bot with interactive menus and per-user wallet management Transform Polybot from single-user CLI to multi-user Telegram bot: - SQLite database for multi-user storage with encrypted private keys (AES-256-GCM) - Interactive inline keyboard menus for wallet, settings, trading, and position management - Per-user bot instances with independent trade monitoring and execution - Full settings management via Telegram (slippage, risk, filters, sizing, keywords) - Wallet connection flow with support for EOA, Email, and Browser wallet types - Follow/unfollow traders with per-trader allocation percentages - Position viewing, individual sell, and sell-all functionality - Run with: npm run telegram https://claude.ai/code/session_012j2JT3oXieZBydkazQxYSY --- .env.example | 11 +- PLAN.md | 62 +++++ package-lock.json | 397 +++++++++++++++++++++++++++++ package.json | 3 + src/db/userDb.ts | 378 +++++++++++++++++++++++++++ src/multiUserBot.ts | 33 +++ src/services/userBotManager.ts | 342 +++++++++++++++++++++++++ src/telegram/bot.ts | 65 +++++ src/telegram/handlers/follow.ts | 213 ++++++++++++++++ src/telegram/handlers/help.ts | 70 +++++ src/telegram/handlers/positions.ts | 197 ++++++++++++++ src/telegram/handlers/settings.ts | 265 +++++++++++++++++++ src/telegram/handlers/start.ts | 40 +++ src/telegram/handlers/stats.ts | 68 +++++ src/telegram/handlers/trading.ts | 96 +++++++ src/telegram/handlers/wallet.ts | 238 +++++++++++++++++ src/telegram/menus.ts | 182 +++++++++++++ 17 files changed, 2657 insertions(+), 3 deletions(-) create mode 100644 PLAN.md create mode 100644 src/db/userDb.ts create mode 100644 src/multiUserBot.ts create mode 100644 src/services/userBotManager.ts create mode 100644 src/telegram/bot.ts create mode 100644 src/telegram/handlers/follow.ts create mode 100644 src/telegram/handlers/help.ts create mode 100644 src/telegram/handlers/positions.ts create mode 100644 src/telegram/handlers/settings.ts create mode 100644 src/telegram/handlers/start.ts create mode 100644 src/telegram/handlers/stats.ts create mode 100644 src/telegram/handlers/trading.ts create mode 100644 src/telegram/handlers/wallet.ts create mode 100644 src/telegram/menus.ts diff --git a/.env.example b/.env.example index 6f36768..59eee18 100644 --- a/.env.example +++ b/.env.example @@ -91,13 +91,18 @@ RETRY_DELAY_MS=2000 # Delay before copying a trade in milliseconds COPY_DELAY_MS=0 -# === TELEGRAM NOTIFICATIONS === -# Get token from @BotFather on Telegram +# === TELEGRAM BOT === +# Get token from @BotFather on Telegram (REQUIRED for multi-user mode) TELEGRAM_BOT_TOKEN= -# Get your chat ID from @userinfobot on Telegram +# Get your chat ID from @userinfobot on Telegram (single-user mode only) TELEGRAM_CHAT_ID= +# === MULTI-USER ENCRYPTION === +# Master key for encrypting user private keys (recommended: generate a random 32+ char string) +# If not set, falls back to TELEGRAM_BOT_TOKEN (less secure) +ENCRYPTION_KEY= + # === HEALTH CHECK ENDPOINT === # Enable HTTP health check server HEALTH_CHECK_ENABLED=false diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..82c724c --- /dev/null +++ b/PLAN.md @@ -0,0 +1,62 @@ +# Polybot Multi-User Telegram Bot - Implementation Plan + +## Overview +Transform Polybot from a single-user CLI bot into a multi-user Telegram bot where each user can: +- Connect their Polymarket wallet (private key + wallet address) +- Configure their own trading settings via Telegram buttons +- Follow different trader wallets independently +- Manage risk settings (slippage, caps, stop-loss, take-profit, etc.) + +## Architecture Changes + +### 1. User Database (`src/db/userDb.ts`) +- SQLite database (via `better-sqlite3`) for persistent multi-user storage +- Tables: `users`, `user_wallets`, `user_settings`, `user_tracked_wallets`, `user_positions`, `user_trades` +- Each user identified by Telegram chat ID +- Encrypted private key storage (AES-256-GCM with master key from env) + +### 2. Telegram Bot Rewrite (`src/telegram/`) +- **bot.ts** - Main Telegram bot with callback query routing +- **menus.ts** - Inline keyboard menu builders +- **handlers/** - Command and callback handlers: + - `start.ts` - /start welcome + main menu + - `wallet.ts` - Connect/disconnect/view wallets + - `follow.ts` - Add/remove/list tracked wallets + - `settings.ts` - Trading settings (slippage, caps, sizing, risk) + - `positions.ts` - View positions, manual sell + - `stats.ts` - Trading stats and P&L + - `trading.ts` - Start/stop/pause trading + +### 3. Per-User Bot Instances (`src/services/userBotManager.ts`) +- Manages a lightweight bot instance per active user +- Each instance runs its own TradeMonitor, Trader, RiskManager +- Lazy initialization (only when user starts trading) +- Graceful shutdown per user + +### 4. Menu Flow +``` +/start โ†’ Main Menu + โ”œโ”€โ”€ ๐Ÿ”‘ Wallets โ†’ Connect Wallet | View Wallets | Disconnect + โ”œโ”€โ”€ ๐Ÿ‘ Follow Traders โ†’ Add Wallet | List | Remove + โ”œโ”€โ”€ โš™๏ธ Settings + โ”‚ โ”œโ”€โ”€ Position Sizing โ†’ Account Size | Max Position | Min Trade | Max % + โ”‚ โ”œโ”€โ”€ Risk Management โ†’ Stop Loss | Take Profit | Trailing Stop | Daily Loss Limit + โ”‚ โ”œโ”€โ”€ Trade Filters โ†’ Min/Max Probability | Blacklist | Whitelist + โ”‚ โ”œโ”€โ”€ Execution โ†’ Slippage | Copy Sells | Conflict Strategy + โ”‚ โ””โ”€โ”€ Time Exits โ†’ Max Hold Time + โ”œโ”€โ”€ ๐Ÿ“Š Positions โ†’ List | Sell Position | Sell All + โ”œโ”€โ”€ ๐Ÿ“ˆ Stats โ†’ Overview | Per-Trader | Reset + โ”œโ”€โ”€ โ–ถ๏ธ Start Trading / โธ Pause / โน Stop + โ””โ”€โ”€ โ“ Help +``` + +## Implementation Steps + +1. Add dependencies (better-sqlite3, encryption) +2. Create database schema and UserDb class +3. Build Telegram menu system with inline keyboards +4. Implement wallet connection flow (encrypted storage) +5. Implement settings management via Telegram +6. Create UserBotManager for per-user bot instances +7. Wire everything together +8. Update config for multi-user mode diff --git a/package-lock.json b/package-lock.json index 6f2bf09..b7f2b79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@scure/bip39": "2.0.1", "@types/node": "25.3.5", "axios": "1.13.6", + "better-sqlite3": "^12.8.0", "dotenv": "17.3.1", "node-telegram-bot-api": "^0.67.0", "tsx": "4.21.0", @@ -22,6 +23,7 @@ "ws": "8.19.0" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/node-telegram-bot-api": "^0.64.0", "@types/ws": "8.18.1" } @@ -665,6 +667,16 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/caseless": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", @@ -906,6 +918,26 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -915,6 +947,29 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/better-sqlite3": { + "version": "12.8.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/bl": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", @@ -937,6 +992,30 @@ "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==", "license": "MIT" }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -990,6 +1069,12 @@ "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", "license": "Apache-2.0" }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1080,6 +1165,30 @@ "ms": "^2.1.1" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -1123,6 +1232,15 @@ "node": ">=0.4.0" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dotenv": { "version": "17.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", @@ -1357,6 +1475,15 @@ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -1395,6 +1522,12 @@ "node": ">=0.10.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -1455,6 +1588,12 @@ "node": ">= 6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1591,6 +1730,12 @@ "assert-plus": "^1.0.0" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/globalthis": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", @@ -1736,12 +1881,38 @@ "node": ">=0.10" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -2199,12 +2370,57 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-telegram-bot-api": { "version": "0.67.0", "resolved": "https://registry.npmjs.org/node-telegram-bot-api/-/node-telegram-bot-api-0.67.0.tgz", @@ -2389,6 +2605,43 @@ "node": ">= 0.4" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -2453,6 +2706,21 @@ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "license": "MIT" }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -2751,6 +3019,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -2869,6 +3149,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/sshpk": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", @@ -2987,6 +3312,78 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/tldts": { "version": "6.1.86", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", diff --git a/package.json b/package.json index 8453bfb..9f702e1 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "build": "tsc", "start": "node dist/bot.js", "bot": "tsx src/bot.ts", + "telegram": "tsx src/multiUserBot.ts", "positions": "tsx src/index.ts positions", "trades": "tsx src/index.ts trades", "status": "tsx src/scripts/status.ts", @@ -56,6 +57,7 @@ "@scure/bip39": "2.0.1", "@types/node": "25.3.5", "axios": "1.13.6", + "better-sqlite3": "^12.8.0", "dotenv": "17.3.1", "node-telegram-bot-api": "^0.67.0", "tsx": "4.21.0", @@ -64,6 +66,7 @@ "ws": "8.19.0" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/node-telegram-bot-api": "^0.64.0", "@types/ws": "8.18.1" } diff --git a/src/db/userDb.ts b/src/db/userDb.ts new file mode 100644 index 0000000..4ed5ada --- /dev/null +++ b/src/db/userDb.ts @@ -0,0 +1,378 @@ +import Database from 'better-sqlite3'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const DB_PATH = join(__dirname, '../../data/users.db'); + +// Encryption helpers for private keys +const ALGORITHM = 'aes-256-gcm'; + +function getEncryptionKey(): Buffer { + const masterKey = process.env.ENCRYPTION_KEY || process.env.TELEGRAM_BOT_TOKEN || 'polybot-default-key-change-me'; + return scryptSync(masterKey, 'polybot-salt', 32); +} + +function encrypt(text: string): string { + const key = getEncryptionKey(); + const iv = randomBytes(16); + const cipher = createCipheriv(ALGORITHM, key, iv); + let encrypted = cipher.update(text, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + const authTag = cipher.getAuthTag().toString('hex'); + return `${iv.toString('hex')}:${authTag}:${encrypted}`; +} + +function decrypt(encryptedText: string): string { + const key = getEncryptionKey(); + const [ivHex, authTagHex, encrypted] = encryptedText.split(':'); + const iv = Buffer.from(ivHex, 'hex'); + const authTag = Buffer.from(authTagHex, 'hex'); + const decipher = createDecipheriv(ALGORITHM, key, iv); + decipher.setAuthTag(authTag); + let decrypted = decipher.update(encrypted, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + return decrypted; +} + +export interface UserRecord { + chatId: string; + username: string | null; + createdAt: string; + isActive: boolean; +} + +export interface UserWallet { + id: number; + chatId: string; + walletAddress: string; + encryptedPrivateKey: string; + funderAddress: string | null; + signatureType: number; + alias: string; + isDefault: boolean; + createdAt: string; +} + +export interface UserTrackedWallet { + id: number; + chatId: string; + walletAddress: string; + alias: string; + allocation: number; + enabled: boolean; +} + +export interface UserSettings { + chatId: string; + enableTrading: boolean; + dryRun: boolean; + userAccountSize: number; + maxPositionSize: number; + minTradeSize: number; + maxPercentagePerTrade: number; + minProbability: number; + maxProbability: number; + maxOpenPositions: number; + stopLossPercent: number; + takeProfitPercent: number; + trailingStopPercent: number; + orderSlippagePercent: number; + copySells: boolean; + maxHoldTimeHours: number; + dailyLossLimit: number; + maxPositionValue: number; + conflictStrategy: string; + blacklistKeywords: string; + whitelistKeywords: string; +} + +const DEFAULT_SETTINGS: Omit = { + enableTrading: false, + dryRun: true, + userAccountSize: 30, + maxPositionSize: 10, + minTradeSize: 1, + maxPercentagePerTrade: 20, + minProbability: 0.05, + maxProbability: 0.95, + maxOpenPositions: 0, + stopLossPercent: -25, + takeProfitPercent: 100, + trailingStopPercent: 15, + orderSlippagePercent: 15, + copySells: true, + maxHoldTimeHours: 0, + dailyLossLimit: 0, + maxPositionValue: 0, + conflictStrategy: 'first', + blacklistKeywords: '', + whitelistKeywords: '', +}; + +export class UserDb { + private db: Database.Database; + + constructor() { + this.db = new Database(DB_PATH); + this.db.pragma('journal_mode = WAL'); + this.db.pragma('foreign_keys = ON'); + this.initSchema(); + } + + private initSchema(): void { + this.db.exec(` + CREATE TABLE IF NOT EXISTS users ( + chat_id TEXT PRIMARY KEY, + username TEXT, + created_at TEXT DEFAULT (datetime('now')), + is_active INTEGER DEFAULT 1 + ); + + CREATE TABLE IF NOT EXISTS user_wallets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + chat_id TEXT NOT NULL REFERENCES users(chat_id), + wallet_address TEXT NOT NULL, + encrypted_private_key TEXT NOT NULL, + funder_address TEXT, + signature_type INTEGER DEFAULT 2, + alias TEXT DEFAULT 'Main', + is_default INTEGER DEFAULT 0, + created_at TEXT DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS user_tracked_wallets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + chat_id TEXT NOT NULL REFERENCES users(chat_id), + wallet_address TEXT NOT NULL, + alias TEXT NOT NULL, + allocation INTEGER DEFAULT 100, + enabled INTEGER DEFAULT 1, + UNIQUE(chat_id, wallet_address) + ); + + CREATE TABLE IF NOT EXISTS user_settings ( + chat_id TEXT PRIMARY KEY REFERENCES users(chat_id), + enable_trading INTEGER DEFAULT 0, + dry_run INTEGER DEFAULT 1, + user_account_size REAL DEFAULT 30, + max_position_size REAL DEFAULT 10, + min_trade_size REAL DEFAULT 1, + max_percentage_per_trade REAL DEFAULT 20, + min_probability REAL DEFAULT 0.05, + max_probability REAL DEFAULT 0.95, + max_open_positions INTEGER DEFAULT 0, + stop_loss_percent REAL DEFAULT -25, + take_profit_percent REAL DEFAULT 100, + trailing_stop_percent REAL DEFAULT 15, + order_slippage_percent REAL DEFAULT 15, + copy_sells INTEGER DEFAULT 1, + max_hold_time_hours REAL DEFAULT 0, + daily_loss_limit REAL DEFAULT 0, + max_position_value REAL DEFAULT 0, + conflict_strategy TEXT DEFAULT 'first', + blacklist_keywords TEXT DEFAULT '', + whitelist_keywords TEXT DEFAULT '' + ); + + CREATE TABLE IF NOT EXISTS user_state ( + chat_id TEXT PRIMARY KEY REFERENCES users(chat_id), + state_json TEXT DEFAULT '{}' + ); + `); + } + + // ===== Users ===== + + ensureUser(chatId: string, username?: string): void { + this.db.prepare(` + INSERT INTO users (chat_id, username) VALUES (?, ?) + ON CONFLICT(chat_id) DO UPDATE SET username = COALESCE(?, username) + `).run(chatId, username || null, username || null); + + // Ensure settings row exists + this.db.prepare(` + INSERT OR IGNORE INTO user_settings (chat_id) VALUES (?) + `).run(chatId); + + // Ensure state row exists + this.db.prepare(` + INSERT OR IGNORE INTO user_state (chat_id) VALUES (?) + `).run(chatId); + } + + getAllActiveUsers(): UserRecord[] { + return this.db.prepare(` + SELECT chat_id as chatId, username, created_at as createdAt, is_active as isActive + FROM users WHERE is_active = 1 + `).all() as UserRecord[]; + } + + // ===== Wallets ===== + + addWallet(chatId: string, privateKey: string, walletAddress: string, funderAddress?: string, alias?: string, signatureType?: number): number { + const encryptedKey = encrypt(privateKey); + const isDefault = this.getWallets(chatId).length === 0 ? 1 : 0; + + const result = this.db.prepare(` + INSERT INTO user_wallets (chat_id, wallet_address, encrypted_private_key, funder_address, signature_type, alias, is_default) + VALUES (?, ?, ?, ?, ?, ?, ?) + `).run(chatId, walletAddress.toLowerCase(), encryptedKey, funderAddress || null, signatureType ?? 2, alias || 'Main', isDefault); + + return result.lastInsertRowid as number; + } + + getWallets(chatId: string): UserWallet[] { + return this.db.prepare(` + SELECT id, chat_id as chatId, wallet_address as walletAddress, + encrypted_private_key as encryptedPrivateKey, funder_address as funderAddress, + signature_type as signatureType, alias, is_default as isDefault, created_at as createdAt + FROM user_wallets WHERE chat_id = ? + `).all(chatId) as UserWallet[]; + } + + getDefaultWallet(chatId: string): UserWallet | undefined { + return this.db.prepare(` + SELECT id, chat_id as chatId, wallet_address as walletAddress, + encrypted_private_key as encryptedPrivateKey, funder_address as funderAddress, + signature_type as signatureType, alias, is_default as isDefault, created_at as createdAt + FROM user_wallets WHERE chat_id = ? AND is_default = 1 + `).get(chatId) as UserWallet | undefined; + } + + decryptPrivateKey(encryptedKey: string): string { + return decrypt(encryptedKey); + } + + removeWallet(chatId: string, walletId: number): boolean { + const result = this.db.prepare(` + DELETE FROM user_wallets WHERE id = ? AND chat_id = ? + `).run(walletId, chatId); + return result.changes > 0; + } + + setDefaultWallet(chatId: string, walletId: number): void { + this.db.prepare(`UPDATE user_wallets SET is_default = 0 WHERE chat_id = ?`).run(chatId); + this.db.prepare(`UPDATE user_wallets SET is_default = 1 WHERE id = ? AND chat_id = ?`).run(walletId, chatId); + } + + // ===== Tracked Wallets ===== + + addTrackedWallet(chatId: string, address: string, alias: string, allocation: number = 100): number { + const result = this.db.prepare(` + INSERT INTO user_tracked_wallets (chat_id, wallet_address, alias, allocation) + VALUES (?, ?, ?, ?) + ON CONFLICT(chat_id, wallet_address) DO UPDATE SET alias = ?, allocation = ?, enabled = 1 + `).run(chatId, address.toLowerCase(), alias, allocation, alias, allocation); + return result.lastInsertRowid as number; + } + + getTrackedWallets(chatId: string): UserTrackedWallet[] { + return this.db.prepare(` + SELECT id, chat_id as chatId, wallet_address as walletAddress, alias, allocation, enabled + FROM user_tracked_wallets WHERE chat_id = ? + `).all(chatId) as UserTrackedWallet[]; + } + + removeTrackedWallet(chatId: string, walletId: number): boolean { + const result = this.db.prepare(` + DELETE FROM user_tracked_wallets WHERE id = ? AND chat_id = ? + `).run(walletId, chatId); + return result.changes > 0; + } + + toggleTrackedWallet(chatId: string, walletId: number): boolean { + this.db.prepare(` + UPDATE user_tracked_wallets SET enabled = CASE WHEN enabled = 1 THEN 0 ELSE 1 END + WHERE id = ? AND chat_id = ? + `).run(walletId, chatId); + const wallet = this.db.prepare(`SELECT enabled FROM user_tracked_wallets WHERE id = ?`).get(walletId) as any; + return wallet?.enabled === 1; + } + + // ===== Settings ===== + + getSettings(chatId: string): UserSettings { + const row = this.db.prepare(` + SELECT chat_id as chatId, enable_trading as enableTrading, dry_run as dryRun, + user_account_size as userAccountSize, max_position_size as maxPositionSize, + min_trade_size as minTradeSize, max_percentage_per_trade as maxPercentagePerTrade, + min_probability as minProbability, max_probability as maxProbability, + max_open_positions as maxOpenPositions, stop_loss_percent as stopLossPercent, + take_profit_percent as takeProfitPercent, trailing_stop_percent as trailingStopPercent, + order_slippage_percent as orderSlippagePercent, copy_sells as copySells, + max_hold_time_hours as maxHoldTimeHours, daily_loss_limit as dailyLossLimit, + max_position_value as maxPositionValue, conflict_strategy as conflictStrategy, + blacklist_keywords as blacklistKeywords, whitelist_keywords as whitelistKeywords + FROM user_settings WHERE chat_id = ? + `).get(chatId) as UserSettings | undefined; + + if (!row) { + return { chatId, ...DEFAULT_SETTINGS }; + } + + // Convert SQLite integers to booleans + return { + ...row, + enableTrading: !!row.enableTrading, + dryRun: !!row.dryRun, + copySells: !!row.copySells, + }; + } + + updateSetting(chatId: string, key: string, value: any): void { + // Map camelCase to snake_case column names + const columnMap: Record = { + enableTrading: 'enable_trading', + dryRun: 'dry_run', + userAccountSize: 'user_account_size', + maxPositionSize: 'max_position_size', + minTradeSize: 'min_trade_size', + maxPercentagePerTrade: 'max_percentage_per_trade', + minProbability: 'min_probability', + maxProbability: 'max_probability', + maxOpenPositions: 'max_open_positions', + stopLossPercent: 'stop_loss_percent', + takeProfitPercent: 'take_profit_percent', + trailingStopPercent: 'trailing_stop_percent', + orderSlippagePercent: 'order_slippage_percent', + copySells: 'copy_sells', + maxHoldTimeHours: 'max_hold_time_hours', + dailyLossLimit: 'daily_loss_limit', + maxPositionValue: 'max_position_value', + conflictStrategy: 'conflict_strategy', + blacklistKeywords: 'blacklist_keywords', + whitelistKeywords: 'whitelist_keywords', + }; + + const column = columnMap[key]; + if (!column) throw new Error(`Unknown setting: ${key}`); + + // Convert booleans to integers for SQLite + const dbValue = typeof value === 'boolean' ? (value ? 1 : 0) : value; + + this.db.prepare(`UPDATE user_settings SET ${column} = ? WHERE chat_id = ?`).run(dbValue, chatId); + } + + // ===== User State (positions, orders, stats) ===== + + getUserState(chatId: string): any { + const row = this.db.prepare(`SELECT state_json FROM user_state WHERE chat_id = ?`).get(chatId) as any; + try { + return row ? JSON.parse(row.state_json) : {}; + } catch { + return {}; + } + } + + saveUserState(chatId: string, state: any): void { + this.db.prepare(` + UPDATE user_state SET state_json = ? WHERE chat_id = ? + `).run(JSON.stringify(state), chatId); + } + + close(): void { + this.db.close(); + } +} diff --git a/src/multiUserBot.ts b/src/multiUserBot.ts new file mode 100644 index 0000000..d2b6e12 --- /dev/null +++ b/src/multiUserBot.ts @@ -0,0 +1,33 @@ +import 'dotenv/config'; +import { TelegramTradingBot } from './telegram/bot.js'; + +async function main() { + console.log('='.repeat(50)); + console.log(' Polybot - Multi-User Telegram Trading Bot'); + console.log('='.repeat(50)); + + const token = process.env.TELEGRAM_BOT_TOKEN; + if (!token) { + console.error('Error: TELEGRAM_BOT_TOKEN environment variable is required'); + console.error('Get a token from @BotFather on Telegram'); + process.exit(1); + } + + const bot = new TelegramTradingBot(token); + console.log('[Main] Telegram bot started. Send /start to your bot to begin.'); + + // Graceful shutdown + const shutdown = async () => { + console.log('\n[Main] Shutting down...'); + await bot.stop(); + process.exit(0); + }; + + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); +} + +main().catch((error) => { + console.error('[Main] Fatal error:', error); + process.exit(1); +}); diff --git a/src/services/userBotManager.ts b/src/services/userBotManager.ts new file mode 100644 index 0000000..7bd5cdf --- /dev/null +++ b/src/services/userBotManager.ts @@ -0,0 +1,342 @@ +import { TradeMonitor } from './tradeMonitor.js'; +import { PositionSizer } from './positionSizer.js'; +import { OrderQueue, QueuedOrder } from './orderQueue.js'; +import { RiskManager } from './riskManager.js'; +import { Trader, TradeResult } from './trader.js'; +import { ClobApiClient } from '../api/clobApi.js'; +import { isMarketBlacklisted, isMarketWhitelisted } from './tradeFilter.js'; +import { UserDb, UserSettings } from '../db/userDb.js'; +import { TradeSignal, WalletConfig, CopyConfig } from '../types/index.js'; +import TelegramBot from 'node-telegram-bot-api'; + +export interface UserBotInstance { + chatId: string; + monitor: TradeMonitor | null; + trader: Trader | null; + orderQueue: OrderQueue; + positionSizer: PositionSizer; + isRunning: boolean; +} + +export class UserBotManager { + private userBots = new Map(); + private db: UserDb; + private telegramBot: TelegramBot; + + constructor(db: UserDb, telegramBot: TelegramBot) { + this.db = db; + this.telegramBot = telegramBot; + } + + isUserRunning(chatId: string): boolean { + return this.userBots.get(chatId)?.isRunning || false; + } + + getUserBot(chatId: string): UserBotInstance | undefined { + return this.userBots.get(chatId); + } + + async startUserBot(chatId: string): Promise { + if (this.userBots.get(chatId)?.isRunning) { + throw new Error('Already running'); + } + + const settings = this.db.getSettings(chatId); + const wallets = this.db.getWallets(chatId); + const trackedWallets = this.db.getTrackedWallets(chatId).filter(t => t.enabled); + + if (wallets.length === 0) throw new Error('No wallet connected'); + if (trackedWallets.length === 0) throw new Error('No traders to follow'); + + const defaultWallet = this.db.getDefaultWallet(chatId) || wallets[0]; + const privateKey = this.db.decryptPrivateKey(defaultWallet.encryptedPrivateKey); + + // Build wallet configs for TradeMonitor + const walletConfigs: WalletConfig[] = trackedWallets.map(tw => ({ + address: tw.walletAddress, + alias: tw.alias, + enabled: true, + allocation: tw.allocation, + })); + + // Create position sizer + const positionSizer = new PositionSizer({ + userAccountSize: settings.userAccountSize, + maxPositionSize: settings.maxPositionSize, + minTradeSize: settings.minTradeSize, + maxPercentage: settings.maxPercentagePerTrade, + }); + + // Create order queue + const orderQueue = new OrderQueue({ + maxConcurrent: 3, + orderDelayMs: 200, + }); + + // Create trader + const trader = new Trader({ + privateKey, + funderAddress: defaultWallet.funderAddress || undefined, + dryRun: settings.dryRun, + signatureType: defaultWallet.signatureType, + orderSlippagePercent: settings.orderSlippagePercent, + maxBuyPrice: settings.maxProbability, + }); + + await trader.initialize(); + + // Create trade monitor + const monitor = new TradeMonitor({ + wallets: walletConfigs, + pollingIntervalMs: 1000, + }); + + const instance: UserBotInstance = { + chatId, + monitor, + trader, + orderQueue, + positionSizer, + isRunning: true, + }; + + // Handle trade signals + monitor.on('trade', async (signal: TradeSignal) => { + await this.handleTradeSignal(chatId, instance, signal, settings); + }); + + // Handle order processing + orderQueue.on('process', async (order: QueuedOrder) => { + await this.executeOrder(chatId, instance, order); + }); + + this.userBots.set(chatId, instance); + + // Start monitoring + await monitor.start(); + + // Load persisted state (last seen trades) + const state = this.db.getUserState(chatId); + if (state.lastSeenTrades) { + for (const [addr, ts] of Object.entries(state.lastSeenTrades)) { + (monitor as any).lastSeenTrades?.set(addr, ts); + } + } + + await this.sendNotification(chatId, + `๐ŸŸข Trading Started!\n\n` + + `Mode: ${settings.dryRun ? '๐Ÿ”ถ Dry Run' : '๐ŸŸข LIVE'}\n` + + `Following: ${trackedWallets.map(t => t.alias).join(', ')}\n` + + `Max position: $${settings.maxPositionSize}` + ); + } + + async stopUserBot(chatId: string): Promise { + const instance = this.userBots.get(chatId); + if (!instance) return; + + instance.isRunning = false; + + if (instance.monitor) { + instance.monitor.stop(); + } + + // Save state before stopping + this.saveUserState(chatId, instance); + + this.userBots.delete(chatId); + + await this.sendNotification(chatId, 'โน Trading Stopped'); + } + + async stopAll(): Promise { + for (const chatId of this.userBots.keys()) { + await this.stopUserBot(chatId); + } + } + + private async handleTradeSignal( + chatId: string, + instance: UserBotInstance, + signal: TradeSignal, + settings: UserSettings + ): Promise { + const trade = signal.trade; + + // Apply filters + const blacklist = settings.blacklistKeywords ? settings.blacklistKeywords.split(',').map(k => k.trim()).filter(Boolean) : []; + const whitelist = settings.whitelistKeywords ? settings.whitelistKeywords.split(',').map(k => k.trim()).filter(Boolean) : []; + + if (blacklist.length > 0 && isMarketBlacklisted(trade.title, blacklist)) { + console.log(`[User ${chatId}] Skipping blacklisted market: ${trade.title}`); + return; + } + + if (whitelist.length > 0 && !isMarketWhitelisted(trade.title, whitelist)) { + console.log(`[User ${chatId}] Skipping non-whitelisted market: ${trade.title}`); + return; + } + + // Probability filter + const price = parseFloat(trade.price); + if (price < settings.minProbability || price > settings.maxProbability) { + console.log(`[User ${chatId}] Skipping: price ${price} outside range [${settings.minProbability}, ${settings.maxProbability}]`); + return; + } + + // Skip sells if copySells is disabled + if (trade.side === 'SELL' && !settings.copySells) { + console.log(`[User ${chatId}] Skipping sell (copySells disabled)`); + return; + } + + // Check max open positions + const state = this.db.getUserState(chatId); + const positions = state.positions || {}; + if (settings.maxOpenPositions > 0 && Object.keys(positions).length >= settings.maxOpenPositions) { + console.log(`[User ${chatId}] Skipping: max open positions reached`); + return; + } + + // Calculate position size + const trackedWallets = this.db.getTrackedWallets(chatId); + const trackerWallet = trackedWallets.find(w => w.walletAddress === trade.proxyWallet); + const allocation = trackerWallet?.allocation || 100; + const tradeUsd = parseFloat(trade.size) * parseFloat(trade.price); + const scaledAmount = Math.min( + tradeUsd * (allocation / 100), + settings.maxPositionSize, + settings.userAccountSize * (settings.maxPercentagePerTrade / 100) + ); + + if (scaledAmount < settings.minTradeSize) { + console.log(`[User ${chatId}] Skipping: scaled amount $${scaledAmount.toFixed(2)} below minimum`); + return; + } + + // Enqueue order + instance.orderQueue.enqueue( + trade, + trackerWallet?.alias || 'Unknown', + trade.proxyWallet, + scaledAmount, + ); + } + + private async executeOrder(chatId: string, instance: UserBotInstance, order: QueuedOrder): Promise { + if (!instance.trader) return; + + const trade = order.trade; + const price = parseFloat(trade.price); + + try { + let result: TradeResult; + + if (trade.side === 'BUY') { + result = await instance.trader.copyTrade(trade, order.amount); + } else { + result = await instance.trader.sellPosition( + trade.asset, + parseFloat(trade.size), + price, + trade.title + ); + } + + // Update state + const state = this.db.getUserState(chatId); + if (!state.positions) state.positions = {}; + if (!state.stats) state.stats = { totalTrades: 0, successfulTrades: 0, failedTrades: 0, totalVolume: 0, startTime: Date.now(), lastUpdateTime: Date.now() }; + if (!state.orders) state.orders = []; + + state.stats.totalTrades++; + state.stats.lastUpdateTime = Date.now(); + + if (result.success) { + state.stats.successfulTrades++; + state.stats.totalVolume += order.amount; + + if (trade.side === 'BUY') { + state.positions[trade.asset] = { + asset: trade.asset, + size: order.amount / price, + avgPrice: price, + title: trade.title, + outcome: trade.outcome, + slug: trade.slug, + entryTime: Date.now(), + walletAlias: order.walletAlias, + conditionId: trade.conditionId, + }; + } else { + delete state.positions[trade.asset]; + } + + // Notify success + const emoji = trade.side === 'BUY' ? '๐ŸŸข' : '๐Ÿ”ด'; + await this.sendNotification(chatId, + `${emoji} Trade Executed\n\n` + + `Side: ${trade.side}\n` + + `Market: ${trade.title}\n` + + `Outcome: ${trade.outcome}\n` + + `Amount: $${order.amount.toFixed(2)}\n` + + `Price: ${(price * 100).toFixed(1)}%\n` + + `Copied from: ${order.walletAlias}` + ); + } else { + state.stats.failedTrades++; + await this.sendNotification(chatId, + `โŒ Trade Failed\n\n` + + `Side: ${trade.side}\n` + + `Market: ${trade.title}\n` + + `Error: ${result.error}` + ); + } + + // Keep last 100 orders + state.orders.unshift({ + id: order.id, + asset: trade.asset, + side: trade.side, + amount: order.amount, + status: result.success ? 'filled' : 'failed', + createdAt: Date.now(), + walletAlias: order.walletAlias, + }); + if (state.orders.length > 100) state.orders = state.orders.slice(0, 100); + + this.db.saveUserState(chatId, state); + } catch (error: any) { + console.error(`[User ${chatId}] Order execution error:`, error.message); + await this.sendNotification(chatId, `โŒ Order error: ${error.message}`); + } + } + + private saveUserState(chatId: string, instance: UserBotInstance): void { + try { + const state = this.db.getUserState(chatId); + // Save last seen trade timestamps + if (instance.monitor) { + const lastSeen: Record = {}; + const monitorMap = (instance.monitor as any).lastSeenTrades; + if (monitorMap) { + for (const [addr, ts] of monitorMap.entries()) { + lastSeen[addr] = ts as number; + } + } + state.lastSeenTrades = lastSeen; + } + this.db.saveUserState(chatId, state); + } catch (error: any) { + console.error(`[User ${chatId}] Failed to save state:`, error.message); + } + } + + private async sendNotification(chatId: string, message: string): Promise { + try { + await this.telegramBot.sendMessage(chatId, message, { parse_mode: 'HTML' }); + } catch (error: any) { + console.error(`[User ${chatId}] Failed to send notification:`, error.message); + } + } +} diff --git a/src/telegram/bot.ts b/src/telegram/bot.ts new file mode 100644 index 0000000..d9f66d8 --- /dev/null +++ b/src/telegram/bot.ts @@ -0,0 +1,65 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { UserDb } from '../db/userDb.js'; +import { UserBotManager } from '../services/userBotManager.js'; +import { registerStartHandler } from './handlers/start.js'; +import { registerWalletHandler } from './handlers/wallet.js'; +import { registerFollowHandler } from './handlers/follow.js'; +import { registerSettingsHandler } from './handlers/settings.js'; +import { registerPositionsHandler } from './handlers/positions.js'; +import { registerStatsHandler } from './handlers/stats.js'; +import { registerTradingHandler } from './handlers/trading.js'; +import { registerHelpHandler } from './handlers/help.js'; + +export class TelegramTradingBot { + private bot: TelegramBot; + private db: UserDb; + private botManager: UserBotManager; + + constructor(token: string) { + this.bot = new TelegramBot(token, { polling: true }); + this.db = new UserDb(); + this.botManager = new UserBotManager(this.db, this.bot); + + this.registerHandlers(); + this.setupErrorHandling(); + } + + private registerHandlers(): void { + const getBotManager = () => this.botManager; + + registerStartHandler(this.bot, this.db); + registerWalletHandler(this.bot, this.db); + registerFollowHandler(this.bot, this.db); + registerSettingsHandler(this.bot, this.db); + registerPositionsHandler(this.bot, this.db, getBotManager); + registerStatsHandler(this.bot, this.db); + registerTradingHandler(this.bot, this.db, getBotManager); + registerHelpHandler(this.bot); + } + + private setupErrorHandling(): void { + this.bot.on('polling_error', (error) => { + console.error('[TelegramBot] Polling error:', error.message); + }); + + this.bot.on('error', (error) => { + console.error('[TelegramBot] Error:', error.message); + }); + } + + async stop(): Promise { + console.log('[TelegramBot] Stopping...'); + await this.botManager.stopAll(); + await this.bot.stopPolling(); + this.db.close(); + console.log('[TelegramBot] Stopped'); + } + + getDb(): UserDb { + return this.db; + } + + getBotManager(): UserBotManager { + return this.botManager; + } +} diff --git a/src/telegram/handlers/follow.ts b/src/telegram/handlers/follow.ts new file mode 100644 index 0000000..de14c89 --- /dev/null +++ b/src/telegram/handlers/follow.ts @@ -0,0 +1,213 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { UserDb } from '../../db/userDb.js'; +import { followMenu, backButton } from '../menus.js'; + +const followFlowState = new Map(); + +export function registerFollowHandler(bot: TelegramBot, db: UserDb): void { + + bot.on('callback_query', async (query) => { + if (!query.data || !query.message) return; + const chatId = query.message.chat.id.toString(); + + if (query.data === 'menu:follow') { + const tracked = db.getTrackedWallets(chatId); + await bot.editMessageText( + '๐Ÿ‘ Follow Traders\n\n' + + 'Add Polymarket wallets to copy-trade from.\n' + + `Currently following: ${tracked.filter(t => t.enabled).length} trader(s)`, + { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: followMenu(tracked.length > 0), + } + ); + await bot.answerCallbackQuery(query.id); + } + + if (query.data === 'follow:add') { + followFlowState.set(chatId, { step: 'address' }); + await bot.editMessageText( + 'โž• Add Trader\n\n' + + 'Send the Polymarket wallet address to follow (0x...):\n\n' + + 'You can find trader addresses on polymarket.com/profile/[address]', + { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: backButton('menu:follow'), + } + ); + await bot.answerCallbackQuery(query.id); + } + + if (query.data === 'follow:list') { + const tracked = db.getTrackedWallets(chatId); + if (tracked.length === 0) { + await bot.editMessageText('No traders being followed.', { + chat_id: chatId, + message_id: query.message.message_id, + reply_markup: followMenu(false), + }); + } else { + let text = '๐Ÿ“‹ Followed Traders\n\n'; + tracked.forEach((t, i) => { + const short = `${t.walletAddress.slice(0, 6)}...${t.walletAddress.slice(-4)}`; + const status = t.enabled ? 'โœ…' : 'โŒ'; + text += `${i + 1}. ${status} ${t.alias}\n`; + text += ` Address: ${short}\n`; + text += ` Allocation: ${t.allocation}%\n\n`; + }); + + const buttons = tracked.map(t => [ + { + text: `${t.enabled ? 'โŒ Disable' : 'โœ… Enable'} ${t.alias}`, + callback_data: `follow:toggle:${t.id}`, + }, + ]); + buttons.push([{ text: 'โฌ…๏ธ Back', callback_data: 'menu:follow' }]); + + await bot.editMessageText(text, { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: { inline_keyboard: buttons }, + }); + } + await bot.answerCallbackQuery(query.id); + } + + if (query.data?.startsWith('follow:toggle:')) { + const id = parseInt(query.data.split(':')[2]); + const enabled = db.toggleTrackedWallet(chatId, id); + await bot.answerCallbackQuery(query.id, { text: enabled ? 'Trader enabled' : 'Trader disabled' }); + // Refresh the list + const tracked = db.getTrackedWallets(chatId); + let text = '๐Ÿ“‹ Followed Traders\n\n'; + tracked.forEach((t, i) => { + const short = `${t.walletAddress.slice(0, 6)}...${t.walletAddress.slice(-4)}`; + const status = t.enabled ? 'โœ…' : 'โŒ'; + text += `${i + 1}. ${status} ${t.alias}\n`; + text += ` Address: ${short}\n`; + text += ` Allocation: ${t.allocation}%\n\n`; + }); + const buttons = tracked.map(t => [ + { + text: `${t.enabled ? 'โŒ Disable' : 'โœ… Enable'} ${t.alias}`, + callback_data: `follow:toggle:${t.id}`, + }, + ]); + buttons.push([{ text: 'โฌ…๏ธ Back', callback_data: 'menu:follow' }]); + await bot.editMessageText(text, { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: { inline_keyboard: buttons }, + }); + } + + if (query.data === 'follow:remove_list') { + const tracked = db.getTrackedWallets(chatId); + const buttons = tracked.map(t => { + const short = `${t.walletAddress.slice(0, 6)}...${t.walletAddress.slice(-4)}`; + return [{ text: `๐Ÿ—‘ ${t.alias} (${short})`, callback_data: `follow:remove:${t.id}` }]; + }); + buttons.push([{ text: 'โฌ…๏ธ Back', callback_data: 'menu:follow' }]); + await bot.editMessageText('๐Ÿ—‘ Remove Trader\n\nSelect trader to remove:', { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: { inline_keyboard: buttons }, + }); + await bot.answerCallbackQuery(query.id); + } + + if (query.data?.startsWith('follow:remove:')) { + const id = parseInt(query.data.split(':')[2]); + db.removeTrackedWallet(chatId, id); + const tracked = db.getTrackedWallets(chatId); + await bot.editMessageText('โœ… Trader removed.', { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: followMenu(tracked.length > 0), + }); + await bot.answerCallbackQuery(query.id); + } + }); + + // Handle text messages for follow flow + bot.on('message', async (msg) => { + if (!msg.text || msg.text.startsWith('/')) return; + const chatId = msg.chat.id.toString(); + const state = followFlowState.get(chatId); + if (!state) return; + + if (state.step === 'address') { + const addr = msg.text.trim(); + if (!addr.startsWith('0x') || addr.length !== 42) { + await bot.sendMessage(chatId, 'โŒ Invalid address. Must be 42 characters starting with 0x.\n\nTry again:', { + reply_markup: backButton('menu:follow'), + }); + return; + } + state.address = addr; + state.step = 'alias'; + await bot.sendMessage(chatId, + `โœ… Address: ${addr.slice(0, 6)}...${addr.slice(-4)}\n\n` + + `Send a name/alias for this trader (e.g., "Whale1", "TopTrader"):`, + { parse_mode: 'HTML', reply_markup: backButton('menu:follow') } + ); + return; + } + + if (state.step === 'alias') { + const alias = msg.text.trim().slice(0, 20); + state.step = 'allocation'; + followFlowState.set(chatId, { ...state, step: 'allocation' }); + await bot.sendMessage(chatId, + `Name: ${alias}\n\n` + + `Send allocation percentage (1-100):\n` + + `This controls what % of calculated position size to use for this trader.\n` + + `(100 = full size, 50 = half size)`, + { parse_mode: 'HTML', reply_markup: backButton('menu:follow') } + ); + // Store alias temporarily + (state as any).alias = alias; + return; + } + + if (state.step === 'allocation') { + const allocation = parseInt(msg.text.trim()); + if (isNaN(allocation) || allocation < 1 || allocation > 100) { + await bot.sendMessage(chatId, 'โŒ Invalid allocation. Enter a number between 1 and 100:', { + reply_markup: backButton('menu:follow'), + }); + return; + } + + try { + db.addTrackedWallet(chatId, state.address!, (state as any).alias, allocation); + followFlowState.delete(chatId); + + const tracked = db.getTrackedWallets(chatId); + await bot.sendMessage(chatId, + `โœ… Trader Added!\n\n` + + `Name: ${(state as any).alias}\n` + + `Address: ${state.address!.slice(0, 6)}...${state.address!.slice(-4)}\n` + + `Allocation: ${allocation}%`, + { + parse_mode: 'HTML', + reply_markup: followMenu(tracked.length > 0), + } + ); + } catch (err: any) { + await bot.sendMessage(chatId, `โŒ Error: ${err.message}`, { + reply_markup: backButton('menu:follow'), + }); + } + return; + } + }); +} diff --git a/src/telegram/handlers/help.ts b/src/telegram/handlers/help.ts new file mode 100644 index 0000000..0b84af4 --- /dev/null +++ b/src/telegram/handlers/help.ts @@ -0,0 +1,70 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { mainMenu } from '../menus.js'; + +export function registerHelpHandler(bot: TelegramBot): void { + + bot.on('callback_query', async (query) => { + if (!query.data || !query.message) return; + const chatId = query.message.chat.id.toString(); + + if (query.data === 'menu:help') { + const helpText = ` +โ“ Polybot Help + +Getting Started: +1. ๐Ÿ”‘ Connect your Polymarket wallet +2. ๐Ÿ‘ Add traders to follow +3. โš™๏ธ Configure your settings +4. โ–ถ๏ธ Start trading! + +How it works: +โ€ข Monitors followed traders in real-time +โ€ข Copies their trades to your wallet +โ€ข Scales position sizes to your account +โ€ข Manages exits via stop-loss, take-profit, trailing stops + +Wallet Types: +โ€ข Browser - MetaMask/Rabby with Polymarket proxy +โ€ข Email - Magic Link exported key +โ€ข EOA - Standalone wallet + +Key Settings: +โ€ข Slippage - Max price deviation allowed +โ€ข Stop Loss - Auto-sell at loss threshold +โ€ข Take Profit - Auto-sell at profit target +โ€ข Trailing Stop - Lock in profits as price rises +โ€ข Dry Run - Test without real trades + +Commands: +/start - Main menu +/menu - Show menu +/help - This help message + +Tips: +โ€ข Start with Dry Run mode ON +โ€ข Use small position sizes initially +โ€ข Diversify across multiple traders +โ€ข Set stop-loss to protect capital + `.trim(); + + await bot.editMessageText(helpText, { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: mainMenu(), + }); + await bot.answerCallbackQuery(query.id); + } + + // Handle main menu callback + if (query.data === 'menu:main') { + await bot.editMessageText('๐Ÿค– Main Menu', { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: mainMenu(), + }); + await bot.answerCallbackQuery(query.id); + } + }); +} diff --git a/src/telegram/handlers/positions.ts b/src/telegram/handlers/positions.ts new file mode 100644 index 0000000..0f19843 --- /dev/null +++ b/src/telegram/handlers/positions.ts @@ -0,0 +1,197 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { UserDb } from '../../db/userDb.js'; +import { positionsMenu, confirmMenu, backButton } from '../menus.js'; +import type { UserBotManager } from '../../services/userBotManager.js'; + +export function registerPositionsHandler(bot: TelegramBot, db: UserDb, getBotManager: () => UserBotManager): void { + + bot.on('callback_query', async (query) => { + if (!query.data || !query.message) return; + const chatId = query.message.chat.id.toString(); + + if (query.data === 'menu:positions') { + const state = db.getUserState(chatId); + const positions = state.positions ? Object.values(state.positions) : []; + const hasPositions = positions.length > 0; + + await bot.editMessageText( + `๐Ÿ“Š Positions\n\nOpen positions: ${positions.length}`, + { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: positionsMenu(hasPositions), + } + ); + await bot.answerCallbackQuery(query.id); + } + + if (query.data === 'positions:list') { + const state = db.getUserState(chatId); + const positions = state.positions ? Object.entries(state.positions) : []; + + if (positions.length === 0) { + await bot.editMessageText('๐Ÿ“Š No open positions', { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: positionsMenu(false), + }); + } else { + let text = '๐Ÿ“Š Open Positions\n\n'; + (positions as [string, any][]).forEach(([asset, pos], i) => { + text += `${i + 1}. ${pos.outcome}\n`; + text += ` Market: ${pos.title?.slice(0, 40) || 'Unknown'}\n`; + text += ` Size: ${pos.size?.toFixed(4) || '?'} shares @ $${pos.avgPrice?.toFixed(3) || '?'}\n`; + text += ` Value: $${((pos.size || 0) * (pos.avgPrice || 0)).toFixed(2)}\n`; + if (pos.walletAlias) text += ` Copied from: ${pos.walletAlias}\n`; + text += '\n'; + }); + + await bot.editMessageText(text, { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: positionsMenu(true), + }); + } + await bot.answerCallbackQuery(query.id); + } + + if (query.data === 'positions:sell_list') { + const state = db.getUserState(chatId); + const positions = state.positions ? Object.entries(state.positions) : []; + + if (positions.length === 0) { + await bot.answerCallbackQuery(query.id, { text: 'No positions to sell' }); + return; + } + + const buttons = (positions as [string, any][]).map(([asset, pos]) => [{ + text: `๐Ÿ”ด ${pos.outcome} (${pos.size?.toFixed(2) || '?'} shares)`, + callback_data: `positions:sell:${asset.slice(0, 40)}`, + }]); + buttons.push([{ text: 'โฌ…๏ธ Back', callback_data: 'menu:positions' }]); + + await bot.editMessageText('๐Ÿ”ด Sell Position\n\nSelect position to sell:', { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: { inline_keyboard: buttons }, + }); + await bot.answerCallbackQuery(query.id); + } + + if (query.data?.startsWith('positions:sell:')) { + const assetPrefix = query.data.slice(14); + const botManager = getBotManager(); + const userBot = botManager.getUserBot(chatId); + + if (!userBot) { + await bot.answerCallbackQuery(query.id, { text: 'Trading not active. Start trading first.' }); + return; + } + + await bot.editMessageText(`โณ Selling position...`, { + chat_id: chatId, + message_id: query.message.message_id, + }); + + try { + const state = db.getUserState(chatId); + const positions = state.positions || {}; + const matchingAsset = Object.keys(positions).find(a => a.startsWith(assetPrefix)); + + if (matchingAsset && userBot.trader) { + const pos = positions[matchingAsset]; + const result = await userBot.trader.sellPosition(matchingAsset, pos.size, pos.avgPrice || 0.5, pos.title || 'Unknown'); + if (result.success) { + delete positions[matchingAsset]; + state.positions = positions; + db.saveUserState(chatId, state); + await bot.editMessageText(`โœ… Position sold! Order: ${result.orderId || 'completed'}`, { + chat_id: chatId, + message_id: query.message.message_id, + reply_markup: positionsMenu(Object.keys(positions).length > 0), + }); + } else { + await bot.editMessageText(`โŒ Sell failed: ${result.error}`, { + chat_id: chatId, + message_id: query.message.message_id, + reply_markup: positionsMenu(true), + }); + } + } + } catch (err: any) { + await bot.editMessageText(`โŒ Error: ${err.message}`, { + chat_id: chatId, + message_id: query.message.message_id, + reply_markup: positionsMenu(true), + }); + } + await bot.answerCallbackQuery(query.id); + } + + if (query.data === 'positions:sell_all') { + await bot.editMessageText('โš ๏ธ Sell ALL positions?\n\nThis will sell all your open positions at market price.', { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: confirmMenu('sell_all'), + }); + await bot.answerCallbackQuery(query.id); + } + + if (query.data === 'confirm:sell_all') { + const botManager = getBotManager(); + const userBot = botManager.getUserBot(chatId); + + if (!userBot || !userBot.trader) { + await bot.editMessageText('โŒ Trading not active.', { + chat_id: chatId, + message_id: query.message.message_id, + reply_markup: positionsMenu(false), + }); + await bot.answerCallbackQuery(query.id); + return; + } + + await bot.editMessageText('โณ Selling all positions...', { + chat_id: chatId, + message_id: query.message.message_id, + }); + + const state = db.getUserState(chatId); + const positions = state.positions || {}; + let sold = 0; + let failed = 0; + + for (const [asset, pos] of Object.entries(positions) as [string, any][]) { + try { + const result = await userBot.trader.sellPosition(asset, pos.size, pos.avgPrice || 0.5, pos.title || 'Unknown'); + if (result.success) { + delete positions[asset]; + sold++; + } else { + failed++; + } + } catch { + failed++; + } + } + + state.positions = positions; + db.saveUserState(chatId, state); + + await bot.editMessageText( + `โœ… Sold: ${sold}, Failed: ${failed}`, + { + chat_id: chatId, + message_id: query.message.message_id, + reply_markup: positionsMenu(Object.keys(positions).length > 0), + } + ); + await bot.answerCallbackQuery(query.id); + } + }); +} diff --git a/src/telegram/handlers/settings.ts b/src/telegram/handlers/settings.ts new file mode 100644 index 0000000..b36850f --- /dev/null +++ b/src/telegram/handlers/settings.ts @@ -0,0 +1,265 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { UserDb } from '../../db/userDb.js'; +import { + settingsMenu, sizingMenu, riskMenu, filtersMenu, + executionMenu, timeExitsMenu, keywordsMenu, conflictStrategyMenu, + backButton, +} from '../menus.js'; + +// Track which setting is being edited per user +const pendingInput = new Map(); + +// Setting descriptions and validation +const settingInfo: Record number | null; returnMenu: string }> = { + userAccountSize: { label: 'Account Size', unit: '$', parse: v => { const n = parseFloat(v); return n > 0 ? n : null; }, returnMenu: 'settings:sizing' }, + maxPositionSize: { label: 'Max Position Size', unit: '$', parse: v => { const n = parseFloat(v); return n > 0 ? n : null; }, returnMenu: 'settings:sizing' }, + minTradeSize: { label: 'Min Trade Size', unit: '$', parse: v => { const n = parseFloat(v); return n > 0 ? n : null; }, returnMenu: 'settings:sizing' }, + maxPercentagePerTrade: { label: 'Max % Per Trade', unit: '%', parse: v => { const n = parseFloat(v); return n > 0 && n <= 100 ? n : null; }, returnMenu: 'settings:sizing' }, + maxPositionValue: { label: 'Max Position Value', unit: '$ (0=unlimited)', parse: v => { const n = parseFloat(v); return n >= 0 ? n : null; }, returnMenu: 'settings:sizing' }, + stopLossPercent: { label: 'Stop Loss', unit: '% (negative, e.g. -25)', parse: v => { const n = parseFloat(v); return n < 0 ? n : null; }, returnMenu: 'settings:risk' }, + takeProfitPercent: { label: 'Take Profit', unit: '%', parse: v => { const n = parseFloat(v); return n > 0 ? n : null; }, returnMenu: 'settings:risk' }, + trailingStopPercent: { label: 'Trailing Stop', unit: '% (0=disabled)', parse: v => { const n = parseFloat(v); return n >= 0 ? n : null; }, returnMenu: 'settings:risk' }, + dailyLossLimit: { label: 'Daily Loss Limit', unit: '$ (0=disabled)', parse: v => { const n = parseFloat(v); return n >= 0 ? n : null; }, returnMenu: 'settings:risk' }, + maxOpenPositions: { label: 'Max Open Positions', unit: '(0=unlimited)', parse: v => { const n = parseInt(v); return n >= 0 ? n : null; }, returnMenu: 'settings:risk' }, + minProbability: { label: 'Min Probability', unit: '% (e.g. 5 for 5%)', parse: v => { const n = parseFloat(v); return n >= 0 && n <= 100 ? n / 100 : null; }, returnMenu: 'settings:filters' }, + maxProbability: { label: 'Max Probability', unit: '% (e.g. 95 for 95%)', parse: v => { const n = parseFloat(v); return n > 0 && n <= 100 ? n / 100 : null; }, returnMenu: 'settings:filters' }, + orderSlippagePercent: { label: 'Slippage', unit: '%', parse: v => { const n = parseFloat(v); return n >= 0 && n <= 50 ? n : null; }, returnMenu: 'settings:execution' }, + maxHoldTimeHours: { label: 'Max Hold Time', unit: 'hours (0=disabled)', parse: v => { const n = parseFloat(v); return n >= 0 ? n : null; }, returnMenu: 'settings:time' }, + blacklistKeywords: { label: 'Blacklist Keywords', unit: 'comma-separated', parse: v => null, returnMenu: 'settings:keywords' }, + whitelistKeywords: { label: 'Whitelist Keywords', unit: 'comma-separated', parse: v => null, returnMenu: 'settings:keywords' }, +}; + +export function registerSettingsHandler(bot: TelegramBot, db: UserDb): void { + + bot.on('callback_query', async (query) => { + if (!query.data || !query.message) return; + const chatId = query.message.chat.id.toString(); + const settings = db.getSettings(chatId); + + // Settings main menu + if (query.data === 'menu:settings') { + await bot.editMessageText('โš™๏ธ Settings\n\nConfigure your trading parameters:', { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: settingsMenu(), + }); + await bot.answerCallbackQuery(query.id); + } + + // Sub-menus + if (query.data === 'settings:sizing') { + await bot.editMessageText('๐Ÿ’ฐ Position Sizing\n\nTap a setting to change it:', { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: sizingMenu(settings), + }); + await bot.answerCallbackQuery(query.id); + } + + if (query.data === 'settings:risk') { + await bot.editMessageText('๐Ÿ›ก Risk Management\n\nTap a setting to change it:', { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: riskMenu(settings), + }); + await bot.answerCallbackQuery(query.id); + } + + if (query.data === 'settings:filters') { + await bot.editMessageText('๐Ÿ” Trade Filters\n\nTap a setting to change it:', { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: filtersMenu(settings), + }); + await bot.answerCallbackQuery(query.id); + } + + if (query.data === 'settings:execution') { + await bot.editMessageText('โšก Execution Settings\n\nTap a setting to change it:', { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: executionMenu(settings), + }); + await bot.answerCallbackQuery(query.id); + } + + if (query.data === 'settings:time') { + await bot.editMessageText('โฐ Time-Based Exits\n\nTap a setting to change it:', { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: timeExitsMenu(settings), + }); + await bot.answerCallbackQuery(query.id); + } + + if (query.data === 'settings:keywords') { + await bot.editMessageText( + '๐Ÿ“ Keywords\n\n' + + `Blacklist: ${settings.blacklistKeywords || 'none'}\n` + + `Whitelist: ${settings.whitelistKeywords || 'none'}`, + { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: keywordsMenu(settings), + } + ); + await bot.answerCallbackQuery(query.id); + } + + // Set a specific value + if (query.data?.startsWith('set:')) { + const key = query.data.slice(4); + const info = settingInfo[key]; + if (!info) return; + + pendingInput.set(chatId, { key, returnTo: info.returnMenu }); + + await bot.editMessageText( + `โœ๏ธ Set ${info.label}\n\n` + + `Current value: ${(settings as any)[key]}\n` + + `Enter new value (${info.unit}):`, + { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: backButton(info.returnMenu), + } + ); + await bot.answerCallbackQuery(query.id); + } + + // Toggle boolean settings + if (query.data?.startsWith('toggle:')) { + const key = query.data.slice(7); + const currentValue = (settings as any)[key]; + const newValue = !currentValue; + db.updateSetting(chatId, key, newValue); + + await bot.answerCallbackQuery(query.id, { text: `${key} = ${newValue ? 'ON' : 'OFF'}` }); + + // Refresh the appropriate menu + const updatedSettings = db.getSettings(chatId); + if (key === 'copySells') { + await bot.editMessageText('โšก Execution Settings\n\nTap a setting to change it:', { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: executionMenu(updatedSettings), + }); + } else if (key === 'dryRun') { + const { tradingMenu } = await import('../menus.js'); + await bot.editMessageText( + `โ–ถ๏ธ Trading Control\n\nDry Run: ${updatedSettings.dryRun ? 'โœ… Simulated' : 'โŒ LIVE'}`, + { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: tradingMenu(updatedSettings.enableTrading, updatedSettings.dryRun), + } + ); + } + } + + // Conflict strategy selection + if (query.data === 'set:conflictStrategy') { + await bot.editMessageText( + 'โš”๏ธ Conflict Strategy\n\n' + + 'When followed traders make opposite trades:\n\n' + + 'First - Follow the first signal\n' + + 'Skip - Don\'t trade on conflicts\n' + + 'Majority - Follow the majority\n' + + 'Highest Allocation - Follow the highest-allocated trader', + { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: conflictStrategyMenu(), + } + ); + await bot.answerCallbackQuery(query.id); + } + + if (query.data?.startsWith('conflict:')) { + const strategy = query.data.slice(9); + db.updateSetting(chatId, 'conflictStrategy', strategy); + await bot.answerCallbackQuery(query.id, { text: `Strategy: ${strategy}` }); + const updatedSettings = db.getSettings(chatId); + await bot.editMessageText('โšก Execution Settings\n\nTap a setting to change it:', { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: executionMenu(updatedSettings), + }); + } + }); + + // Handle text input for setting values + bot.on('message', async (msg) => { + if (!msg.text || msg.text.startsWith('/')) return; + const chatId = msg.chat.id.toString(); + const pending = pendingInput.get(chatId); + if (!pending) return; + + const info = settingInfo[pending.key]; + if (!info) { + pendingInput.delete(chatId); + return; + } + + // Handle string settings (keywords) + if (pending.key === 'blacklistKeywords' || pending.key === 'whitelistKeywords') { + const value = msg.text.trim(); + db.updateSetting(chatId, pending.key, value); + pendingInput.delete(chatId); + + const settings = db.getSettings(chatId); + await bot.sendMessage(chatId, + `โœ… ${info.label} updated to: ${value || 'none'}`, + { + parse_mode: 'HTML', + reply_markup: keywordsMenu(settings), + } + ); + return; + } + + // Handle numeric settings + const parsed = info.parse(msg.text.trim()); + if (parsed === null) { + await bot.sendMessage(chatId, `โŒ Invalid value. Expected: ${info.unit}\n\nTry again:`, { + reply_markup: backButton(pending.returnTo), + }); + return; + } + + db.updateSetting(chatId, pending.key, parsed); + pendingInput.delete(chatId); + + const settings = db.getSettings(chatId); + // Send confirmation and show the appropriate sub-menu + const menuFns: Record TelegramBot.InlineKeyboardMarkup> = { + 'settings:sizing': () => sizingMenu(settings), + 'settings:risk': () => riskMenu(settings), + 'settings:filters': () => filtersMenu(settings), + 'settings:execution': () => executionMenu(settings), + 'settings:time': () => timeExitsMenu(settings), + 'settings:keywords': () => keywordsMenu(settings), + }; + + const menuFn = menuFns[pending.returnTo]; + await bot.sendMessage(chatId, + `โœ… ${info.label} updated to: ${parsed}`, + { + parse_mode: 'HTML', + reply_markup: menuFn ? menuFn() : backButton('menu:settings'), + } + ); + }); +} diff --git a/src/telegram/handlers/start.ts b/src/telegram/handlers/start.ts new file mode 100644 index 0000000..d0cd446 --- /dev/null +++ b/src/telegram/handlers/start.ts @@ -0,0 +1,40 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { UserDb } from '../../db/userDb.js'; +import { mainMenu } from '../menus.js'; + +export function registerStartHandler(bot: TelegramBot, db: UserDb): void { + bot.onText(/\/start/, async (msg) => { + const chatId = msg.chat.id.toString(); + const username = msg.from?.username || msg.from?.first_name || null; + db.ensureUser(chatId, username || undefined); + + const wallets = db.getWallets(chatId); + const tracked = db.getTrackedWallets(chatId); + + let welcome = `๐Ÿค– Welcome to Polybot!\n\n`; + welcome += `Multi-user Polymarket copy-trading bot.\n\n`; + welcome += `Status:\n`; + welcome += `โ€ข Wallets: ${wallets.length} connected\n`; + welcome += `โ€ข Following: ${tracked.filter(t => t.enabled).length} traders\n\n`; + + if (wallets.length === 0) { + welcome += `๐Ÿ‘‰ Start by connecting your wallet!\n`; + } + + await bot.sendMessage(chatId, welcome, { + parse_mode: 'HTML', + reply_markup: mainMenu(), + }); + }); + + // Also handle /menu to show main menu + bot.onText(/\/menu/, async (msg) => { + const chatId = msg.chat.id.toString(); + db.ensureUser(chatId, msg.from?.username || undefined); + + await bot.sendMessage(chatId, '๐Ÿค– Main Menu', { + parse_mode: 'HTML', + reply_markup: mainMenu(), + }); + }); +} diff --git a/src/telegram/handlers/stats.ts b/src/telegram/handlers/stats.ts new file mode 100644 index 0000000..64dfd2c --- /dev/null +++ b/src/telegram/handlers/stats.ts @@ -0,0 +1,68 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { UserDb } from '../../db/userDb.js'; +import { backButton } from '../menus.js'; + +export function registerStatsHandler(bot: TelegramBot, db: UserDb): void { + + bot.on('callback_query', async (query) => { + if (!query.data || !query.message) return; + const chatId = query.message.chat.id.toString(); + + if (query.data === 'menu:stats') { + const state = db.getUserState(chatId); + const stats = state.stats || { + totalTrades: 0, + successfulTrades: 0, + failedTrades: 0, + totalVolume: 0, + }; + + const successRate = stats.totalTrades > 0 + ? ((stats.successfulTrades / stats.totalTrades) * 100).toFixed(1) + : '0.0'; + + const positions = state.positions ? Object.values(state.positions) : []; + const totalValue = (positions as any[]).reduce((sum: number, p: any) => sum + (p.size || 0) * (p.avgPrice || 0), 0); + + // Per-trader stats + let traderStatsText = ''; + if (state.traderStats) { + const entries = Object.entries(state.traderStats) as [string, any][]; + if (entries.length > 0) { + traderStatsText = '\n\nPer-Trader Stats:\n'; + entries.forEach(([alias, ts]) => { + const winRate = ts.totalTrades > 0 ? ((ts.wins / ts.totalTrades) * 100).toFixed(0) : '0'; + traderStatsText += `โ€ข ${alias}: ${ts.totalTrades} trades, ${winRate}% win, P&L: $${ts.totalPnL?.toFixed(2) || '0.00'}\n`; + }); + } + } + + const message = ` +๐Ÿ“ˆ Trading Statistics + +Total Trades: ${stats.totalTrades} +Successful: ${stats.successfulTrades} +Failed: ${stats.failedTrades} +Success Rate: ${successRate}% +Total Volume: $${(stats.totalVolume || 0).toFixed(2)} + +Open Positions: ${positions.length} +Total Position Value: $${totalValue.toFixed(2)} +Daily P&L: $${(state.dailyPnL || 0).toFixed(2)}${traderStatsText} + `.trim(); + + const buttons = [ + [{ text: '๐Ÿ”„ Refresh', callback_data: 'menu:stats' }], + [{ text: 'โฌ…๏ธ Back', callback_data: 'menu:main' }], + ]; + + await bot.editMessageText(message, { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: { inline_keyboard: buttons }, + }); + await bot.answerCallbackQuery(query.id); + } + }); +} diff --git a/src/telegram/handlers/trading.ts b/src/telegram/handlers/trading.ts new file mode 100644 index 0000000..568c37d --- /dev/null +++ b/src/telegram/handlers/trading.ts @@ -0,0 +1,96 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { UserDb } from '../../db/userDb.js'; +import { tradingMenu, backButton } from '../menus.js'; +import type { UserBotManager } from '../../services/userBotManager.js'; + +export function registerTradingHandler(bot: TelegramBot, db: UserDb, getBotManager: () => UserBotManager): void { + + bot.on('callback_query', async (query) => { + if (!query.data || !query.message) return; + const chatId = query.message.chat.id.toString(); + + if (query.data === 'menu:trading') { + const settings = db.getSettings(chatId); + const botManager = getBotManager(); + const isRunning = botManager.isUserRunning(chatId); + + const wallets = db.getWallets(chatId); + const tracked = db.getTrackedWallets(chatId).filter(t => t.enabled); + + let statusText = 'โ–ถ๏ธ Trading Control\n\n'; + statusText += `Status: ${isRunning ? '๐ŸŸข RUNNING' : 'โน STOPPED'}\n`; + statusText += `Mode: ${settings.dryRun ? '๐Ÿ”ถ Dry Run' : '๐ŸŸข LIVE'}\n`; + statusText += `Wallets: ${wallets.length}\n`; + statusText += `Following: ${tracked.length} traders\n`; + + if (wallets.length === 0) { + statusText += '\nโš ๏ธ Connect a wallet first!'; + } + if (tracked.length === 0) { + statusText += '\nโš ๏ธ Follow at least one trader first!'; + } + + await bot.editMessageText(statusText, { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: tradingMenu(isRunning, settings.dryRun), + }); + await bot.answerCallbackQuery(query.id); + } + + if (query.data === 'trading:start') { + const wallets = db.getWallets(chatId); + const tracked = db.getTrackedWallets(chatId).filter(t => t.enabled); + + if (wallets.length === 0) { + await bot.answerCallbackQuery(query.id, { text: 'โš ๏ธ Connect a wallet first!', show_alert: true }); + return; + } + if (tracked.length === 0) { + await bot.answerCallbackQuery(query.id, { text: 'โš ๏ธ Follow at least one trader first!', show_alert: true }); + return; + } + + const botManager = getBotManager(); + try { + await botManager.startUserBot(chatId); + const settings = db.getSettings(chatId); + + await bot.editMessageText( + `๐ŸŸข Trading Started!\n\n` + + `Mode: ${settings.dryRun ? '๐Ÿ”ถ Dry Run (simulated)' : '๐ŸŸข LIVE'}\n` + + `Following: ${tracked.length} trader(s)\n` + + `Max position: $${settings.maxPositionSize}`, + { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: tradingMenu(true, settings.dryRun), + } + ); + } catch (err: any) { + await bot.editMessageText(`โŒ Failed to start: ${err.message}`, { + chat_id: chatId, + message_id: query.message.message_id, + reply_markup: tradingMenu(false, db.getSettings(chatId).dryRun), + }); + } + await bot.answerCallbackQuery(query.id); + } + + if (query.data === 'trading:stop') { + const botManager = getBotManager(); + await botManager.stopUserBot(chatId); + const settings = db.getSettings(chatId); + + await bot.editMessageText('โน Trading Stopped', { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: tradingMenu(false, settings.dryRun), + }); + await bot.answerCallbackQuery(query.id); + } + }); +} diff --git a/src/telegram/handlers/wallet.ts b/src/telegram/handlers/wallet.ts new file mode 100644 index 0000000..06e118c --- /dev/null +++ b/src/telegram/handlers/wallet.ts @@ -0,0 +1,238 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { UserDb } from '../../db/userDb.js'; +import { walletsMenu, walletTypeMenu, backButton } from '../menus.js'; +import { privateKeyToAccount } from 'viem/accounts'; + +// Track conversation state for wallet connection flow +const walletFlowState = new Map(); + +export function registerWalletHandler(bot: TelegramBot, db: UserDb): void { + + // Handle menu:wallets callback + bot.on('callback_query', async (query) => { + if (!query.data || !query.message) return; + const chatId = query.message.chat.id.toString(); + + if (query.data === 'menu:wallets') { + const wallets = db.getWallets(chatId); + await bot.editMessageText('๐Ÿ”‘ Wallet Management\n\nConnect your Polymarket wallet to start trading.', { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: walletsMenu(wallets.length > 0), + }); + await bot.answerCallbackQuery(query.id); + } + + if (query.data === 'wallet:connect') { + await bot.editMessageText( + '๐Ÿ”‘ Connect Wallet\n\n' + + 'Select your wallet type:\n\n' + + '๐ŸฆŠ Browser Wallet - MetaMask/Rabby with Polymarket proxy (most common)\n' + + '๐Ÿ“ง Email Wallet - Magic Link login, exported private key\n' + + '๐Ÿ” EOA - Standalone wallet, signer is funder', + { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: walletTypeMenu(), + } + ); + await bot.answerCallbackQuery(query.id); + } + + // Wallet type selection + if (query.data?.startsWith('wallet:type:')) { + const signatureType = parseInt(query.data.split(':')[2]); + walletFlowState.set(chatId, { step: 'private_key', signatureType }); + + await bot.editMessageText( + '๐Ÿ” Step 1: Private Key\n\n' + + 'Send your private key (starts with 0x).\n\n' + + 'โš ๏ธ Your key will be encrypted and stored securely.\n' + + 'โš ๏ธ Delete your message after sending!', + { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: backButton('menu:wallets'), + } + ); + await bot.answerCallbackQuery(query.id); + } + + // List wallets + if (query.data === 'wallet:list') { + const wallets = db.getWallets(chatId); + if (wallets.length === 0) { + await bot.editMessageText('No wallets connected.', { + chat_id: chatId, + message_id: query.message.message_id, + reply_markup: walletsMenu(false), + }); + } else { + let text = '๐Ÿ“‹ Your Wallets\n\n'; + wallets.forEach((w, i) => { + const addr = w.walletAddress; + const short = `${addr.slice(0, 6)}...${addr.slice(-4)}`; + const defaultTag = w.isDefault ? ' โญ Default' : ''; + const typeNames: Record = { 0: 'EOA', 1: 'Email', 2: 'Browser' }; + text += `${i + 1}. ${w.alias}${defaultTag}\n`; + text += ` Address: ${short}\n`; + text += ` Type: ${typeNames[w.signatureType] || 'Unknown'}\n\n`; + }); + await bot.editMessageText(text, { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: walletsMenu(true), + }); + } + await bot.answerCallbackQuery(query.id); + } + + // Remove wallet list + if (query.data === 'wallet:remove_list') { + const wallets = db.getWallets(chatId); + const buttons = wallets.map(w => { + const short = `${w.walletAddress.slice(0, 6)}...${w.walletAddress.slice(-4)}`; + return [{ text: `๐Ÿ—‘ ${w.alias} (${short})`, callback_data: `wallet:remove:${w.id}` }]; + }); + buttons.push([{ text: 'โฌ…๏ธ Back', callback_data: 'menu:wallets' }]); + await bot.editMessageText('๐Ÿ—‘ Remove Wallet\n\nSelect wallet to remove:', { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: { inline_keyboard: buttons }, + }); + await bot.answerCallbackQuery(query.id); + } + + // Remove specific wallet + if (query.data?.startsWith('wallet:remove:')) { + const walletId = parseInt(query.data.split(':')[2]); + db.removeWallet(chatId, walletId); + const wallets = db.getWallets(chatId); + await bot.editMessageText('โœ… Wallet removed successfully.', { + chat_id: chatId, + message_id: query.message.message_id, + parse_mode: 'HTML', + reply_markup: walletsMenu(wallets.length > 0), + }); + await bot.answerCallbackQuery(query.id); + } + }); + + // Handle text messages for wallet connection flow + bot.on('message', async (msg) => { + if (!msg.text || msg.text.startsWith('/')) return; + const chatId = msg.chat.id.toString(); + const state = walletFlowState.get(chatId); + if (!state) return; + + if (state.step === 'private_key') { + const key = msg.text.trim(); + + // Try to delete the message containing the private key for security + try { await bot.deleteMessage(chatId, msg.message_id); } catch {} + + if (!key.startsWith('0x') || key.length < 64) { + await bot.sendMessage(chatId, 'โŒ Invalid private key. Must start with 0x and be 66 characters long.\n\nTry again or press Back.', { + parse_mode: 'HTML', + reply_markup: backButton('menu:wallets'), + }); + return; + } + + try { + // Derive address from private key + const account = privateKeyToAccount(key as `0x${string}`); + state.privateKey = key; + state.walletAddress = account.address; + + if (state.signatureType === 0) { + // EOA: signer is funder, no need for funder address + state.funderAddress = account.address; + state.step = 'alias'; + await bot.sendMessage(chatId, + `โœ… Key accepted!\n\n` + + `Address: ${account.address}\n\n` + + `Step 2: Send a name/alias for this wallet (e.g., "Main", "Trading"):`, + { parse_mode: 'HTML', reply_markup: backButton('menu:wallets') } + ); + } else { + // Need funder/proxy address + state.step = 'funder_address'; + await bot.sendMessage(chatId, + `โœ… Key accepted!\n\n` + + `Signer: ${account.address}\n\n` + + `Step 2: Send your Polymarket proxy/funder wallet address:`, + { parse_mode: 'HTML', reply_markup: backButton('menu:wallets') } + ); + } + } catch (err: any) { + await bot.sendMessage(chatId, `โŒ Invalid private key: ${err.message}\n\nTry again:`, { + reply_markup: backButton('menu:wallets'), + }); + } + return; + } + + if (state.step === 'funder_address') { + const addr = msg.text.trim(); + if (!addr.startsWith('0x') || addr.length !== 42) { + await bot.sendMessage(chatId, 'โŒ Invalid address. Must be 42 characters starting with 0x.\n\nTry again:', { + reply_markup: backButton('menu:wallets'), + }); + return; + } + state.funderAddress = addr; + state.step = 'alias'; + await bot.sendMessage(chatId, + `โœ… Funder address set!\n\n` + + `Step 3: Send a name/alias for this wallet (e.g., "Main", "Trading"):`, + { parse_mode: 'HTML', reply_markup: backButton('menu:wallets') } + ); + return; + } + + if (state.step === 'alias') { + const alias = msg.text.trim().slice(0, 20); + + try { + db.addWallet( + chatId, + state.privateKey!, + state.walletAddress!, + state.funderAddress, + alias, + state.signatureType + ); + + walletFlowState.delete(chatId); + + const short = `${state.walletAddress!.slice(0, 6)}...${state.walletAddress!.slice(-4)}`; + await bot.sendMessage(chatId, + `โœ… Wallet Connected!\n\n` + + `Name: ${alias}\n` + + `Address: ${short}\n\n` + + `You can now follow traders and start copy-trading!`, + { + parse_mode: 'HTML', + reply_markup: walletsMenu(true), + } + ); + } catch (err: any) { + await bot.sendMessage(chatId, `โŒ Error saving wallet: ${err.message}`, { + reply_markup: backButton('menu:wallets'), + }); + } + return; + } + }); +} + +// Export for cleanup +export function clearWalletFlowState(chatId: string): void { + walletFlowState.delete(chatId); +} diff --git a/src/telegram/menus.ts b/src/telegram/menus.ts new file mode 100644 index 0000000..75dad08 --- /dev/null +++ b/src/telegram/menus.ts @@ -0,0 +1,182 @@ +import TelegramBot from 'node-telegram-bot-api'; + +type InlineButton = TelegramBot.InlineKeyboardButton; +type InlineMarkup = TelegramBot.InlineKeyboardMarkup; + +function btn(text: string, callbackData: string): InlineButton { + return { text, callback_data: callbackData }; +} + +export function mainMenu(): InlineMarkup { + return { + inline_keyboard: [ + [btn('๐Ÿ”‘ Wallets', 'menu:wallets'), btn('๐Ÿ‘ Follow Traders', 'menu:follow')], + [btn('โš™๏ธ Settings', 'menu:settings'), btn('๐Ÿ“Š Positions', 'menu:positions')], + [btn('๐Ÿ“ˆ Stats', 'menu:stats'), btn('โ–ถ๏ธ Trading', 'menu:trading')], + [btn('โ“ Help', 'menu:help')], + ], + }; +} + +export function walletsMenu(hasWallets: boolean): InlineMarkup { + const buttons: InlineButton[][] = [ + [btn('โž• Connect Wallet', 'wallet:connect')], + ]; + if (hasWallets) { + buttons.push([btn('๐Ÿ“‹ My Wallets', 'wallet:list'), btn('๐Ÿ—‘ Remove Wallet', 'wallet:remove_list')]); + } + buttons.push([btn('โฌ…๏ธ Back', 'menu:main')]); + return { inline_keyboard: buttons }; +} + +export function walletTypeMenu(): InlineMarkup { + return { + inline_keyboard: [ + [btn('๐ŸฆŠ Browser Wallet (MetaMask/Rabby)', 'wallet:type:2')], + [btn('๐Ÿ“ง Email Wallet (Magic Link)', 'wallet:type:1')], + [btn('๐Ÿ” EOA (Standalone)', 'wallet:type:0')], + [btn('โฌ…๏ธ Back', 'menu:wallets')], + ], + }; +} + +export function followMenu(hasTracked: boolean): InlineMarkup { + const buttons: InlineButton[][] = [ + [btn('โž• Add Trader', 'follow:add')], + ]; + if (hasTracked) { + buttons.push([btn('๐Ÿ“‹ List Traders', 'follow:list'), btn('๐Ÿ—‘ Remove Trader', 'follow:remove_list')]); + } + buttons.push([btn('โฌ…๏ธ Back', 'menu:main')]); + return { inline_keyboard: buttons }; +} + +export function settingsMenu(): InlineMarkup { + return { + inline_keyboard: [ + [btn('๐Ÿ’ฐ Position Sizing', 'settings:sizing'), btn('๐Ÿ›ก Risk Management', 'settings:risk')], + [btn('๐Ÿ” Trade Filters', 'settings:filters'), btn('โšก Execution', 'settings:execution')], + [btn('โฐ Time Exits', 'settings:time'), btn('๐Ÿ“ Keywords', 'settings:keywords')], + [btn('โฌ…๏ธ Back', 'menu:main')], + ], + }; +} + +export function sizingMenu(settings: any): InlineMarkup { + return { + inline_keyboard: [ + [btn(`Account Size: $${settings.userAccountSize}`, 'set:userAccountSize')], + [btn(`Max Position: $${settings.maxPositionSize}`, 'set:maxPositionSize')], + [btn(`Min Trade: $${settings.minTradeSize}`, 'set:minTradeSize')], + [btn(`Max % Per Trade: ${settings.maxPercentagePerTrade}%`, 'set:maxPercentagePerTrade')], + [btn(`Max Position Value: $${settings.maxPositionValue || 'โˆž'}`, 'set:maxPositionValue')], + [btn('โฌ…๏ธ Back', 'menu:settings')], + ], + }; +} + +export function riskMenu(settings: any): InlineMarkup { + return { + inline_keyboard: [ + [btn(`Stop Loss: ${settings.stopLossPercent}%`, 'set:stopLossPercent')], + [btn(`Take Profit: +${settings.takeProfitPercent}%`, 'set:takeProfitPercent')], + [btn(`Trailing Stop: ${settings.trailingStopPercent}%`, 'set:trailingStopPercent')], + [btn(`Daily Loss Limit: $${settings.dailyLossLimit || 'โˆž'}`, 'set:dailyLossLimit')], + [btn(`Max Open Positions: ${settings.maxOpenPositions || 'โˆž'}`, 'set:maxOpenPositions')], + [btn('โฌ…๏ธ Back', 'menu:settings')], + ], + }; +} + +export function filtersMenu(settings: any): InlineMarkup { + return { + inline_keyboard: [ + [btn(`Min Probability: ${(settings.minProbability * 100).toFixed(0)}%`, 'set:minProbability')], + [btn(`Max Probability: ${(settings.maxProbability * 100).toFixed(0)}%`, 'set:maxProbability')], + [btn('โฌ…๏ธ Back', 'menu:settings')], + ], + }; +} + +export function executionMenu(settings: any): InlineMarkup { + return { + inline_keyboard: [ + [btn(`Slippage: ${settings.orderSlippagePercent}%`, 'set:orderSlippagePercent')], + [btn(`Copy Sells: ${settings.copySells ? 'โœ…' : 'โŒ'}`, 'toggle:copySells')], + [btn(`Conflict Strategy: ${settings.conflictStrategy}`, 'set:conflictStrategy')], + [btn('โฌ…๏ธ Back', 'menu:settings')], + ], + }; +} + +export function timeExitsMenu(settings: any): InlineMarkup { + return { + inline_keyboard: [ + [btn(`Max Hold Time: ${settings.maxHoldTimeHours || 'OFF'}h`, 'set:maxHoldTimeHours')], + [btn('โฌ…๏ธ Back', 'menu:settings')], + ], + }; +} + +export function keywordsMenu(settings: any): InlineMarkup { + const bl = settings.blacklistKeywords || 'none'; + const wl = settings.whitelistKeywords || 'none'; + return { + inline_keyboard: [ + [btn(`Blacklist: ${bl.length > 20 ? bl.slice(0, 20) + '...' : bl}`, 'set:blacklistKeywords')], + [btn(`Whitelist: ${wl.length > 20 ? wl.slice(0, 20) + '...' : wl}`, 'set:whitelistKeywords')], + [btn('โฌ…๏ธ Back', 'menu:settings')], + ], + }; +} + +export function conflictStrategyMenu(): InlineMarkup { + return { + inline_keyboard: [ + [btn('First Signal Wins', 'conflict:first')], + [btn('Skip Conflicts', 'conflict:skip')], + [btn('Majority Rules', 'conflict:majority')], + [btn('Highest Allocation', 'conflict:highest_allocation')], + [btn('โฌ…๏ธ Back', 'settings:execution')], + ], + }; +} + +export function tradingMenu(isRunning: boolean, isDryRun: boolean): InlineMarkup { + const buttons: InlineButton[][] = []; + + if (isRunning) { + buttons.push([btn('โน Stop Trading', 'trading:stop')]); + } else { + buttons.push([btn('โ–ถ๏ธ Start Trading', 'trading:start')]); + } + + buttons.push([btn(`Dry Run: ${isDryRun ? 'โœ… ON' : 'โŒ OFF'}`, 'toggle:dryRun')]); + buttons.push([btn('โฌ…๏ธ Back', 'menu:main')]); + + return { inline_keyboard: buttons }; +} + +export function positionsMenu(hasPositions: boolean): InlineMarkup { + const buttons: InlineButton[][] = []; + if (hasPositions) { + buttons.push([btn('๐Ÿ“‹ View All', 'positions:list')]); + buttons.push([btn('๐Ÿ”ด Sell Position', 'positions:sell_list'), btn('๐Ÿ”ด Sell All', 'positions:sell_all')]); + } + buttons.push([btn('โฌ…๏ธ Back', 'menu:main')]); + return { inline_keyboard: buttons }; +} + +export function confirmMenu(action: string): InlineMarkup { + return { + inline_keyboard: [ + [btn('โœ… Yes, confirm', `confirm:${action}`), btn('โŒ Cancel', 'menu:main')], + ], + }; +} + +export function backButton(target: string): InlineMarkup { + return { + inline_keyboard: [[btn('โฌ…๏ธ Back', target)]], + }; +} From 52b14d16aab11b1958a3ceacefa25fba17c8b385 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 21:30:48 +0000 Subject: [PATCH 2/2] Add emojis to all Telegram bot messages, menus, and notifications Enhance the visual appearance of all Telegram messages by adding contextual emojis throughout menus, notifications, and status messages including settings buttons, position details, trade notifications, stats labels, follow/wallet flows, and command help text. https://claude.ai/code/session_01WCuqjynVrtHj3DbKWXmhBz --- src/services/notifier.ts | 112 ++++++++++++++--------------- src/services/userBotManager.ts | 24 +++---- src/telegram/handlers/follow.ts | 24 +++---- src/telegram/handlers/help.ts | 48 ++++++------- src/telegram/handlers/positions.ts | 14 ++-- src/telegram/handlers/settings.ts | 10 +-- src/telegram/handlers/start.ts | 8 +-- src/telegram/handlers/stats.ts | 18 ++--- src/telegram/handlers/trading.ts | 8 +-- src/telegram/handlers/wallet.ts | 22 +++--- src/telegram/menus.ts | 46 ++++++------ 11 files changed, 167 insertions(+), 167 deletions(-) diff --git a/src/services/notifier.ts b/src/services/notifier.ts index 5e68a5b..74a8980 100644 --- a/src/services/notifier.ts +++ b/src/services/notifier.ts @@ -124,8 +124,8 @@ export class TelegramNotifier { const message = ` โœ… Bot Status: ACTIVE -Uptime: ${hours}h ${minutes}m ${seconds}s -Started: ${new Date(this.startTime).toLocaleString()} +โฑ Uptime: ${hours}h ${minutes}m ${seconds}s +๐Ÿ“… Started: ${new Date(this.startTime).toLocaleString()} `.trim(); await this.sendMessage(message); @@ -145,9 +145,9 @@ export class TelegramNotifier { let message = '๐Ÿ“Š Open Positions\n\n'; positions.forEach((pos, i) => { message += `${i + 1}. ${pos.outcome}\n`; - message += ` Market: ${pos.title}\n`; - message += ` Size: ${pos.size.toFixed(4)} shares @ $${pos.avgPrice.toFixed(3)}\n`; - message += ` Value: $${(pos.size * pos.avgPrice).toFixed(2)}\n\n`; + message += ` ๐Ÿช Market: ${pos.title}\n`; + message += ` ๐Ÿ“ฆ Size: ${pos.size.toFixed(4)} shares @ $${pos.avgPrice.toFixed(3)}\n`; + message += ` ๐Ÿ’ต Value: $${(pos.size * pos.avgPrice).toFixed(2)}\n\n`; }); await this.sendMessage(message); @@ -165,11 +165,11 @@ export class TelegramNotifier { const message = ` ๐Ÿ“ˆ Trading Statistics -Total Trades: ${stats.totalTrades} -Successful: ${stats.successfulTrades} -Failed: ${stats.failedTrades} -Success Rate: ${successRate}% -Total Volume: $${stats.totalVolume.toFixed(2)} +๐Ÿ”ข Total Trades: ${stats.totalTrades} +โœ… Successful: ${stats.successfulTrades} +โŒ Failed: ${stats.failedTrades} +๐ŸŽฏ Success Rate: ${successRate}% +๐Ÿ’ฐ Total Volume: $${stats.totalVolume.toFixed(2)} `.trim(); await this.sendMessage(message); @@ -194,7 +194,7 @@ export class TelegramNotifier { return; } - await this.sendMessage(`Found ${summary.count} redeemable position(s) worth ~$${summary.totalValue.toFixed(2)}. Redeeming...`); + await this.sendMessage(`๐ŸŽฏ Found ${summary.count} redeemable position(s) worth ~$${summary.totalValue.toFixed(2)}. Redeeming...`); const results = await this.redeemer.redeemAll(); const succeeded = results.filter(r => r.success); @@ -273,13 +273,13 @@ export class TelegramNotifier { const message = ` ๐Ÿค– Available Commands -/status - Check if bot is active -/positions - Show open positions -/stats - Show trading statistics -/balance - Show balance & redeemable positions -/redeem - Redeem all resolved winning positions -/stop - Stop the bot (auto-deploy will restart) -/help - Show this help message +/status - โœ… Check if bot is active +/positions - ๐Ÿ“Š Show open positions +/stats - ๐Ÿ“ˆ Show trading statistics +/balance - ๐Ÿ’ฐ Show balance & redeemable positions +/redeem - ๐ŸŽฏ Redeem all resolved winning positions +/stop - ๐Ÿ›‘ Stop the bot (auto-deploy will restart) +/help - โ“ Show this help message `.trim(); await this.sendMessage(message); @@ -335,14 +335,14 @@ export class TelegramNotifier { const message = ` ${emoji} Trade Executed -Side: ${trade.side} -Market: ${trade.title} -Outcome: ${trade.outcome} -Amount: $${trade.amount.toFixed(2)} -Price: $${trade.price.toFixed(3)} (${(trade.price * 100).toFixed(1)}%) -${trade.size ? `Size: ${trade.size.toFixed(4)} shares` : ''} -${trade.walletAlias ? `Copied from: ${trade.walletAlias}` : ''} -${trade.orderId ? `Order ID: ${trade.orderId}` : ''} +๐Ÿ“Œ Side: ${trade.side} +๐Ÿช Market: ${trade.title} +๐ŸŽฒ Outcome: ${trade.outcome} +๐Ÿ’ต Amount: $${trade.amount.toFixed(2)} +๐Ÿ’ฒ Price: $${trade.price.toFixed(3)} (${(trade.price * 100).toFixed(1)}%) +${trade.size ? `๐Ÿ“ฆ Size: ${trade.size.toFixed(4)} shares` : ''} +${trade.walletAlias ? `๐Ÿ‘ค Copied from: ${trade.walletAlias}` : ''} +${trade.orderId ? `๐Ÿ†” Order ID: ${trade.orderId}` : ''} `.trim(); await this.sendMessage(message); @@ -355,12 +355,12 @@ ${trade.orderId ? `Order ID: ${trade.orderId}` : ''} const message = ` โŒ Trade Failed -Side: ${trade.side} -Market: ${trade.title} -Outcome: ${trade.outcome} -Amount: $${trade.amount.toFixed(2)} -${trade.walletAlias ? `Copied from: ${trade.walletAlias}` : ''} -Error: ${trade.error} +๐Ÿ“Œ Side: ${trade.side} +๐Ÿช Market: ${trade.title} +๐ŸŽฒ Outcome: ${trade.outcome} +๐Ÿ’ต Amount: $${trade.amount.toFixed(2)} +${trade.walletAlias ? `๐Ÿ‘ค Copied from: ${trade.walletAlias}` : ''} +โš ๏ธ Error: ${trade.error} `.trim(); await this.sendMessage(message); @@ -381,12 +381,12 @@ ${trade.walletAlias ? `Copied from: ${trade.walletAlias}` : ''} const message = ` ${emoji} ${typeLabel} Triggered -Market: ${trigger.title} -Outcome: ${trigger.outcome} -Entry Price: $${trigger.entryPrice.toFixed(3)} -Current Price: $${trigger.currentPrice.toFixed(3)} -P&L: ${pnlSign}${trigger.pnlPercent.toFixed(1)}% -Size: ${trigger.size.toFixed(4)} shares +๐Ÿช Market: ${trigger.title} +๐ŸŽฒ Outcome: ${trigger.outcome} +๐Ÿ“ฅ Entry Price: $${trigger.entryPrice.toFixed(3)} +๐Ÿ“Š Current Price: $${trigger.currentPrice.toFixed(3)} +๐Ÿ’น P&L: ${pnlSign}${trigger.pnlPercent.toFixed(1)}% +๐Ÿ“ฆ Size: ${trigger.size.toFixed(4)} shares ${statusLine} `.trim(); @@ -420,11 +420,11 @@ ${statusLine} const message = ` ๐Ÿš€ Polybot Started -Mode: ${modeEmoji} ${mode} -Tracking: ${config.wallets.join(', ') || 'None'} -Account Size: $${config.accountSize.toFixed(2)} -Max Per Trade: $${config.maxPositionSize.toFixed(2)} -Current Positions: ${config.positionCount} +๐Ÿ”ง Mode: ${modeEmoji} ${mode} +๐Ÿ‘ Tracking: ${config.wallets.join(', ') || 'None'} +๐Ÿ’ต Account Size: $${config.accountSize.toFixed(2)} +๐Ÿ’ฐ Max Per Trade: $${config.maxPositionSize.toFixed(2)} +๐Ÿ“ฆ Current Positions: ${config.positionCount} `.trim(); await this.sendMessage(message); @@ -448,12 +448,12 @@ ${statusLine} message += ` -Session Stats: -โ€ข Total Trades: ${stats.totalTrades} -โ€ข Successful: ${stats.successfulTrades} -โ€ข Failed: ${stats.failedTrades} -โ€ข Success Rate: ${successRate}% -โ€ข Volume: $${stats.totalVolume.toFixed(2)}`; +๐Ÿ“‹ Session Stats: +โ€ข ๐Ÿ”ข Total Trades: ${stats.totalTrades} +โ€ข โœ… Successful: ${stats.successfulTrades} +โ€ข โŒ Failed: ${stats.failedTrades} +โ€ข ๐ŸŽฏ Success Rate: ${successRate}% +โ€ข ๐Ÿ’ฐ Volume: $${stats.totalVolume.toFixed(2)}`; } await this.sendMessage(message); @@ -469,7 +469,7 @@ ${statusLine} let positionsText = ''; if (summary.positions.length === 0) { - positionsText = 'No open positions'; + positionsText = '๐Ÿ“ญ No open positions'; } else { positionsText = summary.positions.map(pos => { const pnlText = pos.pnlPercent !== undefined @@ -482,12 +482,12 @@ ${statusLine} const message = ` ๐Ÿ“Š Daily Summary -Trading Stats: -โ€ข Total Trades: ${summary.totalTrades} -โ€ข Successful: ${summary.successfulTrades} -โ€ข Failed: ${summary.failedTrades} -โ€ข Success Rate: ${successRate}% -โ€ข Volume: $${summary.totalVolume.toFixed(2)} +๐Ÿ“‹ Trading Stats: +โ€ข ๐Ÿ”ข Total Trades: ${summary.totalTrades} +โ€ข โœ… Successful: ${summary.successfulTrades} +โ€ข โŒ Failed: ${summary.failedTrades} +โ€ข ๐ŸŽฏ Success Rate: ${successRate}% +โ€ข ๐Ÿ’ฐ Volume: $${summary.totalVolume.toFixed(2)} Open Positions (${summary.positions.length}): ${positionsText} diff --git a/src/services/userBotManager.ts b/src/services/userBotManager.ts index 7bd5cdf..e91c2a4 100644 --- a/src/services/userBotManager.ts +++ b/src/services/userBotManager.ts @@ -126,8 +126,8 @@ export class UserBotManager { await this.sendNotification(chatId, `๐ŸŸข Trading Started!\n\n` + `Mode: ${settings.dryRun ? '๐Ÿ”ถ Dry Run' : '๐ŸŸข LIVE'}\n` + - `Following: ${trackedWallets.map(t => t.alias).join(', ')}\n` + - `Max position: $${settings.maxPositionSize}` + `๐Ÿ‘ Following: ${trackedWallets.map(t => t.alias).join(', ')}\n` + + `๐Ÿ’ฐ Max position: $${settings.maxPositionSize}` ); } @@ -276,20 +276,20 @@ export class UserBotManager { const emoji = trade.side === 'BUY' ? '๐ŸŸข' : '๐Ÿ”ด'; await this.sendNotification(chatId, `${emoji} Trade Executed\n\n` + - `Side: ${trade.side}\n` + - `Market: ${trade.title}\n` + - `Outcome: ${trade.outcome}\n` + - `Amount: $${order.amount.toFixed(2)}\n` + - `Price: ${(price * 100).toFixed(1)}%\n` + - `Copied from: ${order.walletAlias}` + `๐Ÿ“Œ Side: ${trade.side}\n` + + `๐Ÿช Market: ${trade.title}\n` + + `๐ŸŽฒ Outcome: ${trade.outcome}\n` + + `๐Ÿ’ต Amount: $${order.amount.toFixed(2)}\n` + + `๐Ÿ’ฒ Price: ${(price * 100).toFixed(1)}%\n` + + `๐Ÿ‘ค Copied from: ${order.walletAlias}` ); } else { state.stats.failedTrades++; await this.sendNotification(chatId, `โŒ Trade Failed\n\n` + - `Side: ${trade.side}\n` + - `Market: ${trade.title}\n` + - `Error: ${result.error}` + `๐Ÿ“Œ Side: ${trade.side}\n` + + `๐Ÿช Market: ${trade.title}\n` + + `โš ๏ธ Error: ${result.error}` ); } @@ -308,7 +308,7 @@ export class UserBotManager { this.db.saveUserState(chatId, state); } catch (error: any) { console.error(`[User ${chatId}] Order execution error:`, error.message); - await this.sendNotification(chatId, `โŒ Order error: ${error.message}`); + await this.sendNotification(chatId, `โŒ โš ๏ธ Order error: ${error.message}`); } } diff --git a/src/telegram/handlers/follow.ts b/src/telegram/handlers/follow.ts index de14c89..319a6b7 100644 --- a/src/telegram/handlers/follow.ts +++ b/src/telegram/handlers/follow.ts @@ -45,7 +45,7 @@ export function registerFollowHandler(bot: TelegramBot, db: UserDb): void { if (query.data === 'follow:list') { const tracked = db.getTrackedWallets(chatId); if (tracked.length === 0) { - await bot.editMessageText('No traders being followed.', { + await bot.editMessageText('๐Ÿ“ญ No traders being followed yet.', { chat_id: chatId, message_id: query.message.message_id, reply_markup: followMenu(false), @@ -56,8 +56,8 @@ export function registerFollowHandler(bot: TelegramBot, db: UserDb): void { const short = `${t.walletAddress.slice(0, 6)}...${t.walletAddress.slice(-4)}`; const status = t.enabled ? 'โœ…' : 'โŒ'; text += `${i + 1}. ${status} ${t.alias}\n`; - text += ` Address: ${short}\n`; - text += ` Allocation: ${t.allocation}%\n\n`; + text += ` ๐Ÿ“ Address: ${short}\n`; + text += ` ๐Ÿ“Š Allocation: ${t.allocation}%\n\n`; }); const buttons = tracked.map(t => [ @@ -81,7 +81,7 @@ export function registerFollowHandler(bot: TelegramBot, db: UserDb): void { if (query.data?.startsWith('follow:toggle:')) { const id = parseInt(query.data.split(':')[2]); const enabled = db.toggleTrackedWallet(chatId, id); - await bot.answerCallbackQuery(query.id, { text: enabled ? 'Trader enabled' : 'Trader disabled' }); + await bot.answerCallbackQuery(query.id, { text: enabled ? 'โœ… Trader enabled' : 'โŒ Trader disabled' }); // Refresh the list const tracked = db.getTrackedWallets(chatId); let text = '๐Ÿ“‹ Followed Traders\n\n'; @@ -89,8 +89,8 @@ export function registerFollowHandler(bot: TelegramBot, db: UserDb): void { const short = `${t.walletAddress.slice(0, 6)}...${t.walletAddress.slice(-4)}`; const status = t.enabled ? 'โœ…' : 'โŒ'; text += `${i + 1}. ${status} ${t.alias}\n`; - text += ` Address: ${short}\n`; - text += ` Allocation: ${t.allocation}%\n\n`; + text += ` ๐Ÿ“ Address: ${short}\n`; + text += ` ๐Ÿ“Š Allocation: ${t.allocation}%\n\n`; }); const buttons = tracked.map(t => [ { @@ -156,7 +156,7 @@ export function registerFollowHandler(bot: TelegramBot, db: UserDb): void { state.step = 'alias'; await bot.sendMessage(chatId, `โœ… Address: ${addr.slice(0, 6)}...${addr.slice(-4)}\n\n` + - `Send a name/alias for this trader (e.g., "Whale1", "TopTrader"):`, + `โœ๏ธ Send a name/alias for this trader (e.g., "Whale1", "TopTrader"):`, { parse_mode: 'HTML', reply_markup: backButton('menu:follow') } ); return; @@ -167,8 +167,8 @@ export function registerFollowHandler(bot: TelegramBot, db: UserDb): void { state.step = 'allocation'; followFlowState.set(chatId, { ...state, step: 'allocation' }); await bot.sendMessage(chatId, - `Name: ${alias}\n\n` + - `Send allocation percentage (1-100):\n` + + `๐Ÿท Name: ${alias}\n\n` + + `๐Ÿ“Š Send allocation percentage (1-100):\n` + `This controls what % of calculated position size to use for this trader.\n` + `(100 = full size, 50 = half size)`, { parse_mode: 'HTML', reply_markup: backButton('menu:follow') } @@ -194,9 +194,9 @@ export function registerFollowHandler(bot: TelegramBot, db: UserDb): void { const tracked = db.getTrackedWallets(chatId); await bot.sendMessage(chatId, `โœ… Trader Added!\n\n` + - `Name: ${(state as any).alias}\n` + - `Address: ${state.address!.slice(0, 6)}...${state.address!.slice(-4)}\n` + - `Allocation: ${allocation}%`, + `๐Ÿท Name: ${(state as any).alias}\n` + + `๐Ÿ“ Address: ${state.address!.slice(0, 6)}...${state.address!.slice(-4)}\n` + + `๐Ÿ“Š Allocation: ${allocation}%`, { parse_mode: 'HTML', reply_markup: followMenu(tracked.length > 0), diff --git a/src/telegram/handlers/help.ts b/src/telegram/handlers/help.ts index 0b84af4..7dbcc95 100644 --- a/src/telegram/handlers/help.ts +++ b/src/telegram/handlers/help.ts @@ -17,34 +17,34 @@ export function registerHelpHandler(bot: TelegramBot): void { 3. โš™๏ธ Configure your settings 4. โ–ถ๏ธ Start trading! -How it works: -โ€ข Monitors followed traders in real-time -โ€ข Copies their trades to your wallet -โ€ข Scales position sizes to your account -โ€ข Manages exits via stop-loss, take-profit, trailing stops +๐Ÿ”ง How it works: +โ€ข ๐Ÿ“ก Monitors followed traders in real-time +โ€ข ๐Ÿ”„ Copies their trades to your wallet +โ€ข โš–๏ธ Scales position sizes to your account +โ€ข ๐Ÿ›ก Manages exits via stop-loss, take-profit, trailing stops -Wallet Types: -โ€ข Browser - MetaMask/Rabby with Polymarket proxy -โ€ข Email - Magic Link exported key -โ€ข EOA - Standalone wallet +๐Ÿ’ผ Wallet Types: +โ€ข ๐ŸฆŠ Browser - MetaMask/Rabby with Polymarket proxy +โ€ข ๐Ÿ“ง Email - Magic Link exported key +โ€ข ๐Ÿ” EOA - Standalone wallet -Key Settings: -โ€ข Slippage - Max price deviation allowed -โ€ข Stop Loss - Auto-sell at loss threshold -โ€ข Take Profit - Auto-sell at profit target -โ€ข Trailing Stop - Lock in profits as price rises -โ€ข Dry Run - Test without real trades +โš™๏ธ Key Settings: +โ€ข ๐ŸŽš Slippage - Max price deviation allowed +โ€ข ๐Ÿ”ป Stop Loss - Auto-sell at loss threshold +โ€ข ๐ŸŽฏ Take Profit - Auto-sell at profit target +โ€ข ๐Ÿ“‰ Trailing Stop - Lock in profits as price rises +โ€ข ๐Ÿงช Dry Run - Test without real trades -Commands: -/start - Main menu -/menu - Show menu -/help - This help message +๐Ÿ“‹ Commands: +/start - ๐Ÿ  Main menu +/menu - ๐Ÿ“ฑ Show menu +/help - โ“ This help message -Tips: -โ€ข Start with Dry Run mode ON -โ€ข Use small position sizes initially -โ€ข Diversify across multiple traders -โ€ข Set stop-loss to protect capital +๐Ÿ’ก Tips: +โ€ข ๐Ÿงช Start with Dry Run mode ON +โ€ข ๐Ÿค Use small position sizes initially +โ€ข ๐ŸŒ Diversify across multiple traders +โ€ข ๐Ÿ›ก Set stop-loss to protect capital `.trim(); await bot.editMessageText(helpText, { diff --git a/src/telegram/handlers/positions.ts b/src/telegram/handlers/positions.ts index 0f19843..5226797 100644 --- a/src/telegram/handlers/positions.ts +++ b/src/telegram/handlers/positions.ts @@ -41,10 +41,10 @@ export function registerPositionsHandler(bot: TelegramBot, db: UserDb, getBotMan let text = '๐Ÿ“Š Open Positions\n\n'; (positions as [string, any][]).forEach(([asset, pos], i) => { text += `${i + 1}. ${pos.outcome}\n`; - text += ` Market: ${pos.title?.slice(0, 40) || 'Unknown'}\n`; - text += ` Size: ${pos.size?.toFixed(4) || '?'} shares @ $${pos.avgPrice?.toFixed(3) || '?'}\n`; - text += ` Value: $${((pos.size || 0) * (pos.avgPrice || 0)).toFixed(2)}\n`; - if (pos.walletAlias) text += ` Copied from: ${pos.walletAlias}\n`; + text += ` ๐Ÿช Market: ${pos.title?.slice(0, 40) || 'Unknown'}\n`; + text += ` ๐Ÿ“ฆ Size: ${pos.size?.toFixed(4) || '?'} shares @ $${pos.avgPrice?.toFixed(3) || '?'}\n`; + text += ` ๐Ÿ’ต Value: $${((pos.size || 0) * (pos.avgPrice || 0)).toFixed(2)}\n`; + if (pos.walletAlias) text += ` ๐Ÿ‘ค Copied from: ${pos.walletAlias}\n`; text += '\n'; }); @@ -63,7 +63,7 @@ export function registerPositionsHandler(bot: TelegramBot, db: UserDb, getBotMan const positions = state.positions ? Object.entries(state.positions) : []; if (positions.length === 0) { - await bot.answerCallbackQuery(query.id, { text: 'No positions to sell' }); + await bot.answerCallbackQuery(query.id, { text: '๐Ÿ“ญ No positions to sell' }); return; } @@ -88,7 +88,7 @@ export function registerPositionsHandler(bot: TelegramBot, db: UserDb, getBotMan const userBot = botManager.getUserBot(chatId); if (!userBot) { - await bot.answerCallbackQuery(query.id, { text: 'Trading not active. Start trading first.' }); + await bot.answerCallbackQuery(query.id, { text: 'โš ๏ธ Trading not active. Start trading first.' }); return; } @@ -184,7 +184,7 @@ export function registerPositionsHandler(bot: TelegramBot, db: UserDb, getBotMan db.saveUserState(chatId, state); await bot.editMessageText( - `โœ… Sold: ${sold}, Failed: ${failed}`, + `โœ… Sold: ${sold}${failed > 0 ? `, โŒ Failed: ${failed}` : ''}`, { chat_id: chatId, message_id: query.message.message_id, diff --git a/src/telegram/handlers/settings.ts b/src/telegram/handlers/settings.ts index b36850f..2c4e834 100644 --- a/src/telegram/handlers/settings.ts +++ b/src/telegram/handlers/settings.ts @@ -172,10 +172,10 @@ export function registerSettingsHandler(bot: TelegramBot, db: UserDb): void { await bot.editMessageText( 'โš”๏ธ Conflict Strategy\n\n' + 'When followed traders make opposite trades:\n\n' + - 'First - Follow the first signal\n' + - 'Skip - Don\'t trade on conflicts\n' + - 'Majority - Follow the majority\n' + - 'Highest Allocation - Follow the highest-allocated trader', + '๐Ÿฅ‡ First - Follow the first signal\n' + + 'โญ Skip - Don\'t trade on conflicts\n' + + '๐Ÿ‘ฅ Majority - Follow the majority\n' + + '๐Ÿ’Ž Highest Allocation - Follow the highest-allocated trader', { chat_id: chatId, message_id: query.message.message_id, @@ -189,7 +189,7 @@ export function registerSettingsHandler(bot: TelegramBot, db: UserDb): void { if (query.data?.startsWith('conflict:')) { const strategy = query.data.slice(9); db.updateSetting(chatId, 'conflictStrategy', strategy); - await bot.answerCallbackQuery(query.id, { text: `Strategy: ${strategy}` }); + await bot.answerCallbackQuery(query.id, { text: `โœ… Strategy: ${strategy}` }); const updatedSettings = db.getSettings(chatId); await bot.editMessageText('โšก Execution Settings\n\nTap a setting to change it:', { chat_id: chatId, diff --git a/src/telegram/handlers/start.ts b/src/telegram/handlers/start.ts index d0cd446..a84e1b2 100644 --- a/src/telegram/handlers/start.ts +++ b/src/telegram/handlers/start.ts @@ -12,10 +12,10 @@ export function registerStartHandler(bot: TelegramBot, db: UserDb): void { const tracked = db.getTrackedWallets(chatId); let welcome = `๐Ÿค– Welcome to Polybot!\n\n`; - welcome += `Multi-user Polymarket copy-trading bot.\n\n`; - welcome += `Status:\n`; - welcome += `โ€ข Wallets: ${wallets.length} connected\n`; - welcome += `โ€ข Following: ${tracked.filter(t => t.enabled).length} traders\n\n`; + welcome += `๐Ÿค Multi-user Polymarket copy-trading bot.\n\n`; + welcome += `๐Ÿ“‹ Status:\n`; + welcome += `โ€ข ๐Ÿ’ผ Wallets: ${wallets.length} connected\n`; + welcome += `โ€ข ๐Ÿ‘ Following: ${tracked.filter(t => t.enabled).length} traders\n\n`; if (wallets.length === 0) { welcome += `๐Ÿ‘‰ Start by connecting your wallet!\n`; diff --git a/src/telegram/handlers/stats.ts b/src/telegram/handlers/stats.ts index 64dfd2c..8109705 100644 --- a/src/telegram/handlers/stats.ts +++ b/src/telegram/handlers/stats.ts @@ -29,7 +29,7 @@ export function registerStatsHandler(bot: TelegramBot, db: UserDb): void { if (state.traderStats) { const entries = Object.entries(state.traderStats) as [string, any][]; if (entries.length > 0) { - traderStatsText = '\n\nPer-Trader Stats:\n'; + traderStatsText = '\n\n๐Ÿ‘ฅ Per-Trader Stats:\n'; entries.forEach(([alias, ts]) => { const winRate = ts.totalTrades > 0 ? ((ts.wins / ts.totalTrades) * 100).toFixed(0) : '0'; traderStatsText += `โ€ข ${alias}: ${ts.totalTrades} trades, ${winRate}% win, P&L: $${ts.totalPnL?.toFixed(2) || '0.00'}\n`; @@ -40,15 +40,15 @@ export function registerStatsHandler(bot: TelegramBot, db: UserDb): void { const message = ` ๐Ÿ“ˆ Trading Statistics -Total Trades: ${stats.totalTrades} -Successful: ${stats.successfulTrades} -Failed: ${stats.failedTrades} -Success Rate: ${successRate}% -Total Volume: $${(stats.totalVolume || 0).toFixed(2)} +๐Ÿ”ข Total Trades: ${stats.totalTrades} +โœ… Successful: ${stats.successfulTrades} +โŒ Failed: ${stats.failedTrades} +๐ŸŽฏ Success Rate: ${successRate}% +๐Ÿ’ฐ Total Volume: $${(stats.totalVolume || 0).toFixed(2)} -Open Positions: ${positions.length} -Total Position Value: $${totalValue.toFixed(2)} -Daily P&L: $${(state.dailyPnL || 0).toFixed(2)}${traderStatsText} +๐Ÿ“ฆ Open Positions: ${positions.length} +๐Ÿ’ต Total Position Value: $${totalValue.toFixed(2)} +๐Ÿ“Š Daily P&L: $${(state.dailyPnL || 0).toFixed(2)}${traderStatsText} `.trim(); const buttons = [ diff --git a/src/telegram/handlers/trading.ts b/src/telegram/handlers/trading.ts index 568c37d..46d0f9d 100644 --- a/src/telegram/handlers/trading.ts +++ b/src/telegram/handlers/trading.ts @@ -20,8 +20,8 @@ export function registerTradingHandler(bot: TelegramBot, db: UserDb, getBotManag let statusText = 'โ–ถ๏ธ Trading Control\n\n'; statusText += `Status: ${isRunning ? '๐ŸŸข RUNNING' : 'โน STOPPED'}\n`; statusText += `Mode: ${settings.dryRun ? '๐Ÿ”ถ Dry Run' : '๐ŸŸข LIVE'}\n`; - statusText += `Wallets: ${wallets.length}\n`; - statusText += `Following: ${tracked.length} traders\n`; + statusText += `๐Ÿ’ผ Wallets: ${wallets.length}\n`; + statusText += `๐Ÿ‘ Following: ${tracked.length} traders\n`; if (wallets.length === 0) { statusText += '\nโš ๏ธ Connect a wallet first!'; @@ -60,8 +60,8 @@ export function registerTradingHandler(bot: TelegramBot, db: UserDb, getBotManag await bot.editMessageText( `๐ŸŸข Trading Started!\n\n` + `Mode: ${settings.dryRun ? '๐Ÿ”ถ Dry Run (simulated)' : '๐ŸŸข LIVE'}\n` + - `Following: ${tracked.length} trader(s)\n` + - `Max position: $${settings.maxPositionSize}`, + `๐Ÿ‘ Following: ${tracked.length} trader(s)\n` + + `๐Ÿ’ฐ Max position: $${settings.maxPositionSize}`, { chat_id: chatId, message_id: query.message.message_id, diff --git a/src/telegram/handlers/wallet.ts b/src/telegram/handlers/wallet.ts index 06e118c..f8ce228 100644 --- a/src/telegram/handlers/wallet.ts +++ b/src/telegram/handlers/wallet.ts @@ -65,7 +65,7 @@ export function registerWalletHandler(bot: TelegramBot, db: UserDb): void { if (query.data === 'wallet:list') { const wallets = db.getWallets(chatId); if (wallets.length === 0) { - await bot.editMessageText('No wallets connected.', { + await bot.editMessageText('๐Ÿ“ญ No wallets connected.', { chat_id: chatId, message_id: query.message.message_id, reply_markup: walletsMenu(false), @@ -78,8 +78,8 @@ export function registerWalletHandler(bot: TelegramBot, db: UserDb): void { const defaultTag = w.isDefault ? ' โญ Default' : ''; const typeNames: Record = { 0: 'EOA', 1: 'Email', 2: 'Browser' }; text += `${i + 1}. ${w.alias}${defaultTag}\n`; - text += ` Address: ${short}\n`; - text += ` Type: ${typeNames[w.signatureType] || 'Unknown'}\n\n`; + text += ` ๐Ÿ“ Address: ${short}\n`; + text += ` ๐Ÿท Type: ${typeNames[w.signatureType] || 'Unknown'}\n\n`; }); await bot.editMessageText(text, { chat_id: chatId, @@ -156,8 +156,8 @@ export function registerWalletHandler(bot: TelegramBot, db: UserDb): void { state.step = 'alias'; await bot.sendMessage(chatId, `โœ… Key accepted!\n\n` + - `Address: ${account.address}\n\n` + - `Step 2: Send a name/alias for this wallet (e.g., "Main", "Trading"):`, + `๐Ÿ“ Address: ${account.address}\n\n` + + `Step 2: โœ๏ธ Send a name/alias for this wallet (e.g., "Main", "Trading"):`, { parse_mode: 'HTML', reply_markup: backButton('menu:wallets') } ); } else { @@ -165,8 +165,8 @@ export function registerWalletHandler(bot: TelegramBot, db: UserDb): void { state.step = 'funder_address'; await bot.sendMessage(chatId, `โœ… Key accepted!\n\n` + - `Signer: ${account.address}\n\n` + - `Step 2: Send your Polymarket proxy/funder wallet address:`, + `๐Ÿ“ Signer: ${account.address}\n\n` + + `Step 2: ๐Ÿ“ฌ Send your Polymarket proxy/funder wallet address:`, { parse_mode: 'HTML', reply_markup: backButton('menu:wallets') } ); } @@ -190,7 +190,7 @@ export function registerWalletHandler(bot: TelegramBot, db: UserDb): void { state.step = 'alias'; await bot.sendMessage(chatId, `โœ… Funder address set!\n\n` + - `Step 3: Send a name/alias for this wallet (e.g., "Main", "Trading"):`, + `Step 3: โœ๏ธ Send a name/alias for this wallet (e.g., "Main", "Trading"):`, { parse_mode: 'HTML', reply_markup: backButton('menu:wallets') } ); return; @@ -214,9 +214,9 @@ export function registerWalletHandler(bot: TelegramBot, db: UserDb): void { const short = `${state.walletAddress!.slice(0, 6)}...${state.walletAddress!.slice(-4)}`; await bot.sendMessage(chatId, `โœ… Wallet Connected!\n\n` + - `Name: ${alias}\n` + - `Address: ${short}\n\n` + - `You can now follow traders and start copy-trading!`, + `๐Ÿท Name: ${alias}\n` + + `๐Ÿ“ Address: ${short}\n\n` + + `๐ŸŽ‰ You can now follow traders and start copy-trading!`, { parse_mode: 'HTML', reply_markup: walletsMenu(true), diff --git a/src/telegram/menus.ts b/src/telegram/menus.ts index 75dad08..3d2a8c0 100644 --- a/src/telegram/menus.ts +++ b/src/telegram/menus.ts @@ -65,11 +65,11 @@ export function settingsMenu(): InlineMarkup { export function sizingMenu(settings: any): InlineMarkup { return { inline_keyboard: [ - [btn(`Account Size: $${settings.userAccountSize}`, 'set:userAccountSize')], - [btn(`Max Position: $${settings.maxPositionSize}`, 'set:maxPositionSize')], - [btn(`Min Trade: $${settings.minTradeSize}`, 'set:minTradeSize')], - [btn(`Max % Per Trade: ${settings.maxPercentagePerTrade}%`, 'set:maxPercentagePerTrade')], - [btn(`Max Position Value: $${settings.maxPositionValue || 'โˆž'}`, 'set:maxPositionValue')], + [btn(`๐Ÿ’ต Account Size: $${settings.userAccountSize}`, 'set:userAccountSize')], + [btn(`๐Ÿ“ Max Position: $${settings.maxPositionSize}`, 'set:maxPositionSize')], + [btn(`๐Ÿ”ป Min Trade: $${settings.minTradeSize}`, 'set:minTradeSize')], + [btn(`๐Ÿ“Š Max % Per Trade: ${settings.maxPercentagePerTrade}%`, 'set:maxPercentagePerTrade')], + [btn(`๐Ÿท Max Position Value: $${settings.maxPositionValue || 'โˆž'}`, 'set:maxPositionValue')], [btn('โฌ…๏ธ Back', 'menu:settings')], ], }; @@ -78,11 +78,11 @@ export function sizingMenu(settings: any): InlineMarkup { export function riskMenu(settings: any): InlineMarkup { return { inline_keyboard: [ - [btn(`Stop Loss: ${settings.stopLossPercent}%`, 'set:stopLossPercent')], - [btn(`Take Profit: +${settings.takeProfitPercent}%`, 'set:takeProfitPercent')], - [btn(`Trailing Stop: ${settings.trailingStopPercent}%`, 'set:trailingStopPercent')], - [btn(`Daily Loss Limit: $${settings.dailyLossLimit || 'โˆž'}`, 'set:dailyLossLimit')], - [btn(`Max Open Positions: ${settings.maxOpenPositions || 'โˆž'}`, 'set:maxOpenPositions')], + [btn(`๐Ÿ”ป Stop Loss: ${settings.stopLossPercent}%`, 'set:stopLossPercent')], + [btn(`๐ŸŽฏ Take Profit: +${settings.takeProfitPercent}%`, 'set:takeProfitPercent')], + [btn(`๐Ÿ“‰ Trailing Stop: ${settings.trailingStopPercent}%`, 'set:trailingStopPercent')], + [btn(`๐Ÿšซ Daily Loss Limit: $${settings.dailyLossLimit || 'โˆž'}`, 'set:dailyLossLimit')], + [btn(`๐Ÿ“ฆ Max Open Positions: ${settings.maxOpenPositions || 'โˆž'}`, 'set:maxOpenPositions')], [btn('โฌ…๏ธ Back', 'menu:settings')], ], }; @@ -91,8 +91,8 @@ export function riskMenu(settings: any): InlineMarkup { export function filtersMenu(settings: any): InlineMarkup { return { inline_keyboard: [ - [btn(`Min Probability: ${(settings.minProbability * 100).toFixed(0)}%`, 'set:minProbability')], - [btn(`Max Probability: ${(settings.maxProbability * 100).toFixed(0)}%`, 'set:maxProbability')], + [btn(`โฌ‡๏ธ Min Probability: ${(settings.minProbability * 100).toFixed(0)}%`, 'set:minProbability')], + [btn(`โฌ†๏ธ Max Probability: ${(settings.maxProbability * 100).toFixed(0)}%`, 'set:maxProbability')], [btn('โฌ…๏ธ Back', 'menu:settings')], ], }; @@ -101,9 +101,9 @@ export function filtersMenu(settings: any): InlineMarkup { export function executionMenu(settings: any): InlineMarkup { return { inline_keyboard: [ - [btn(`Slippage: ${settings.orderSlippagePercent}%`, 'set:orderSlippagePercent')], - [btn(`Copy Sells: ${settings.copySells ? 'โœ…' : 'โŒ'}`, 'toggle:copySells')], - [btn(`Conflict Strategy: ${settings.conflictStrategy}`, 'set:conflictStrategy')], + [btn(`๐ŸŽš Slippage: ${settings.orderSlippagePercent}%`, 'set:orderSlippagePercent')], + [btn(`๐Ÿ“ค Copy Sells: ${settings.copySells ? 'โœ…' : 'โŒ'}`, 'toggle:copySells')], + [btn(`โš”๏ธ Conflict Strategy: ${settings.conflictStrategy}`, 'set:conflictStrategy')], [btn('โฌ…๏ธ Back', 'menu:settings')], ], }; @@ -112,7 +112,7 @@ export function executionMenu(settings: any): InlineMarkup { export function timeExitsMenu(settings: any): InlineMarkup { return { inline_keyboard: [ - [btn(`Max Hold Time: ${settings.maxHoldTimeHours || 'OFF'}h`, 'set:maxHoldTimeHours')], + [btn(`โฑ Max Hold Time: ${settings.maxHoldTimeHours || 'OFF'}h`, 'set:maxHoldTimeHours')], [btn('โฌ…๏ธ Back', 'menu:settings')], ], }; @@ -123,8 +123,8 @@ export function keywordsMenu(settings: any): InlineMarkup { const wl = settings.whitelistKeywords || 'none'; return { inline_keyboard: [ - [btn(`Blacklist: ${bl.length > 20 ? bl.slice(0, 20) + '...' : bl}`, 'set:blacklistKeywords')], - [btn(`Whitelist: ${wl.length > 20 ? wl.slice(0, 20) + '...' : wl}`, 'set:whitelistKeywords')], + [btn(`๐Ÿšซ Blacklist: ${bl.length > 20 ? bl.slice(0, 20) + '...' : bl}`, 'set:blacklistKeywords')], + [btn(`โœ… Whitelist: ${wl.length > 20 ? wl.slice(0, 20) + '...' : wl}`, 'set:whitelistKeywords')], [btn('โฌ…๏ธ Back', 'menu:settings')], ], }; @@ -133,10 +133,10 @@ export function keywordsMenu(settings: any): InlineMarkup { export function conflictStrategyMenu(): InlineMarkup { return { inline_keyboard: [ - [btn('First Signal Wins', 'conflict:first')], - [btn('Skip Conflicts', 'conflict:skip')], - [btn('Majority Rules', 'conflict:majority')], - [btn('Highest Allocation', 'conflict:highest_allocation')], + [btn('๐Ÿฅ‡ First Signal Wins', 'conflict:first')], + [btn('โญ Skip Conflicts', 'conflict:skip')], + [btn('๐Ÿ‘ฅ Majority Rules', 'conflict:majority')], + [btn('๐Ÿ’Ž Highest Allocation', 'conflict:highest_allocation')], [btn('โฌ…๏ธ Back', 'settings:execution')], ], }; @@ -151,7 +151,7 @@ export function tradingMenu(isRunning: boolean, isDryRun: boolean): InlineMarkup buttons.push([btn('โ–ถ๏ธ Start Trading', 'trading:start')]); } - buttons.push([btn(`Dry Run: ${isDryRun ? 'โœ… ON' : 'โŒ OFF'}`, 'toggle:dryRun')]); + buttons.push([btn(`๐Ÿงช Dry Run: ${isDryRun ? 'โœ… ON' : 'โŒ OFF'}`, 'toggle:dryRun')]); buttons.push([btn('โฌ…๏ธ Back', 'menu:main')]); return { inline_keyboard: buttons };