From 2f61cd035455c017c9865278b795ff731dc37f6e Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 30 Oct 2025 03:04:09 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E5=8C=96=E9=80=9A=E7=9F=A5=E7=B3=BB=E7=BB=9F=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增英文版插件化通知系统架构设计文档,详细说明系统架构和实现方案 - 包含核心组件说明:通知提供者接口、插件加载器、通知管理器、消息格式化器 - 提供完整的代码示例,包括配置管理扩展、交易执行器重构和Telegram服务适配 - 添加Slack插件包设计方案,包含包结构、配置选项和使用指南 - 详细说明环境变量配置和代码使用方法,便于开发者 --- docs/plugin-notification-system-en.md | 564 ++++++++++++++++++++++++++ docs/plugin-notification-system-zh.md | 564 ++++++++++++++++++++++++++ 2 files changed, 1128 insertions(+) create mode 100644 docs/plugin-notification-system-en.md create mode 100644 docs/plugin-notification-system-zh.md diff --git a/docs/plugin-notification-system-en.md b/docs/plugin-notification-system-en.md new file mode 100644 index 0000000..4c33769 --- /dev/null +++ b/docs/plugin-notification-system-en.md @@ -0,0 +1,564 @@ +# Plugin-based Notification System + +## Overview + +This document describes the plugin-based notification system architecture for nof1-tracker, which transforms the current hardcoded Telegram notifications into an extensible plugin system supporting multiple notification channels like Slack, Discord, etc. + +## Architecture + +### Core Components + +#### 1. Notification Provider Interface + +```typescript +// src/types/notification.ts +export interface TradeNotificationData { + symbol: string; + side: 'BUY' | 'SELL'; + quantity: string; + price: string; + orderId: string; + status: string; + leverage?: number; + marginType?: string; +} + +export interface StopOrderData { + type: 'take_profit' | 'stop_loss'; + symbol: string; + price: string; + orderId: string; +} + +export interface NotificationProvider { + name: string; + sendMessage(message: string): Promise; + isEnabled(): boolean; +} +``` + +#### 2. Plugin Loader + +The PluginLoader dynamically loads notification plugins using runtime interface validation, allowing flexible plugin management without complex dependencies. + +```typescript +// src/utils/plugin-loader.ts +export class PluginLoader { + static async loadNotificationPlugin( + packageName: string, + config?: any + ): Promise { + try { + const pluginModule = await import(packageName); + const ProviderClass = pluginModule.default || + pluginModule[`${packageName.split('-').pop()}Provider`]; + + if (!ProviderClass) { + throw new Error(`Plugin ${packageName} does not export a provider class`); + } + + const provider = new ProviderClass(config); + + // Runtime interface validation + if (this.isValidProvider(provider)) { + console.log(`✅ Loaded notification plugin: ${packageName}`); + return provider; + } + + throw new Error('Plugin does not implement required NotificationProvider interface'); + } catch (error) { + console.warn(`❌ Failed to load plugin ${packageName}:`, error); + return null; + } + } + + private static isValidProvider(obj: any): obj is NotificationProvider { + return obj && + typeof obj.name === 'string' && + typeof obj.sendMessage === 'function' && + typeof obj.isEnabled === 'function'; + } +} +``` + +#### 3. Notification Manager + +The NotificationManager manages multiple notification providers and handles message broadcasting with fault tolerance. + +```typescript +// src/services/notification-manager.ts +export class NotificationManager { + private providers: NotificationProvider[] = []; + + addProvider(provider: NotificationProvider): void { + this.providers.push(provider); + } + + async notifyTrade(data: TradeNotificationData): Promise { + const message = MessageFormatter.formatTradeMessage(data); + await this.broadcastMessage(message); + } + + private async broadcastMessage(message: string): Promise { + const enabledProviders = this.providers.filter(p => p.isEnabled()); + + // Concurrent sending with fault tolerance + const promises = enabledProviders.map(async provider => { + try { + await provider.sendMessage(message); + } catch (error) { + console.error(`Failed to send notification via ${provider.name}:`, error); + } + }); + + await Promise.allSettled(promises); + } +} +``` + +#### 4. Message Formatter + +Centralized message formatting logic that can be reused across different notification providers. + +```typescript +// src/utils/message-formatter.ts +export class MessageFormatter { + static formatTradeMessage(data: TradeNotificationData): string { + const { symbol, side, quantity, price, orderId, status, leverage, marginType } = data; + + const sideEmoji = side === 'BUY' ? '📈' : '📉'; + const sideText = side === 'BUY' ? 'LONG' : 'SHORT'; + + let message = `✅ Trade Executed\n\n`; + message += `${sideEmoji} ${sideText} ${symbol}\n`; + message += `💰 Quantity: ${quantity}\n`; + message += `💵 Price: ${price}\n`; + message += `🆔 Order ID: ${orderId}\n`; + message += `📊 Status: ${status}\n`; + + if (leverage) { + message += `⚡ Leverage: ${leverage}x\n`; + } + + if (marginType) { + const marginTypeText = marginType === 'ISOLATED' ? '🔒 Isolated' : '🔄 Cross'; + message += `${marginTypeText}\n`; + } + + return message; + } +} +``` + +## Implementation in Main Project + +### 1. Configuration Management Extension + +Extend the existing ConfigManager to support multiple notification providers. + +```typescript +// src/services/config-manager.ts +export interface TradingConfig { + // ... existing configurations + notificationPlugins?: Record; + }>; +} + +export class ConfigManager { + loadFromEnvironment(): void { + // Load notification plugin configurations + const pluginConfigs = process.env.NOTIFICATION_PLUGINS; + if (pluginConfigs) { + const plugins = pluginConfigs.split(',').map(p => p.trim()); + this.config.notificationPlugins = {}; + + plugins.forEach(pluginName => { + const enabled = process.env[`${pluginName.toUpperCase()}_ENABLED`] === 'true'; + const packageName = process.env[`${pluginName.toUpperCase()}_PACKAGE`] || `nof1-${pluginName}-plugin`; + + const pluginConfig: Record = {}; + Object.keys(process.env).forEach(key => { + if (key.startsWith(`${pluginName.toUpperCase()}_`) && + !key.endsWith('_ENABLED') && + !key.endsWith('_PACKAGE')) { + const configKey = key.substring(pluginName.length + 1).toLowerCase(); + pluginConfig[configKey] = process.env[key]; + } + }); + + this.config.notificationPlugins[pluginName] = { + enabled, + package: packageName, + config: pluginConfig + }; + }); + } + } +} +``` + +### 2. TradingExecutor Refactoring + +Replace hardcoded Telegram calls with the unified NotificationManager. + +```typescript +// src/services/trading-executor.ts +export class TradingExecutor { + private notificationManager: NotificationManager; + + constructor(..., configManager?: ConfigManager) { + // ... existing code + this.notificationManager = new NotificationManager(); + this.initializeNotifications(); + } + + private async initializeNotifications(): Promise { + // Load built-in Telegram support + const telegramConfig = this.configManager.getConfig().telegram; + if (telegramConfig.enabled) { + const telegramProvider = new TelegramService(telegramConfig.token); + this.notificationManager.addProvider(telegramProvider); + } + + // Load external plugins + const pluginConfigs = this.configManager.getConfig().notificationPlugins; + if (pluginConfigs) { + for (const [pluginName, pluginConfig] of Object.entries(pluginConfigs)) { + if (pluginConfig.enabled) { + const provider = await PluginLoader.loadNotificationPlugin( + pluginConfig.package, + pluginConfig.config + ); + if (provider) { + this.notificationManager.addProvider(provider); + } + } + } + } + } + + // Replace existing Telegram notification calls + private async sendTradeNotification(orderResponse: any, tradingPlan: TradingPlan): Promise { + await this.notificationManager.notifyTrade({ + symbol: orderResponse.symbol, + side: tradingPlan.side, + quantity: orderResponse.executedQty, + price: orderResponse.avgPrice || 'Market', + orderId: orderResponse.orderId.toString(), + status: orderResponse.status, + leverage: tradingPlan.leverage, + marginType: tradingPlan.marginType + }); + } +} +``` + +### 3. Telegram Service Adapter + +Adapt the existing TelegramService to implement the NotificationProvider interface. + +```typescript +// src/services/telegram-service.ts +export class TelegramService implements NotificationProvider { + name = 'telegram'; + private bot: TelegramBot; + + constructor(token: string) { + this.bot = new TelegramBot(token, { polling: false }); + } + + // Implement NotificationProvider interface + async sendMessage(message: string): Promise { + const configManager = new ConfigManager(); + configManager.loadFromEnvironment(); + const telegramConfig = configManager.getConfig().telegram; + + if (telegramConfig.chatId) { + await this.sendMessage(telegramConfig.chatId, message); + } else { + throw new Error('Telegram chatId not configured'); + } + } + + isEnabled(): boolean { + const configManager = new ConfigManager(); + configManager.loadFromEnvironment(); + const telegramConfig = configManager.getConfig().telegram; + return !!(telegramConfig.enabled && telegramConfig.token && telegramConfig.chatId); + } +} +``` + +## Slack Plugin Package Design + +### 1. Package Structure + +``` +nof1-slack-plugin/ +├── package.json +├── README.md +├── tsconfig.json +├── src/ +│ ├── index.ts # Main entry +│ ├── slack-provider.ts # Slack provider implementation +│ └── message-formatter.ts # Slack message formatting +├── dist/ # Compiled output +└── examples/ + └── usage-example.ts # Usage examples +``` + +### 2. package.json + +```json +{ + "name": "nof1-slack-plugin", + "version": "1.0.0", + "description": "Slack notification plugin for nof1-tracker", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "test": "jest" + }, + "keywords": ["nof1", "slack", "notifications", "trading"], + "author": "Your Name", + "license": "MIT", + "dependencies": { + "@slack/web-api": "^6.9.0" + }, + "devDependencies": { + "@types/node": "^18.0.0", + "typescript": "^4.9.0" + } +} +``` + +### 3. Slack Provider Implementation + +```typescript +// src/slack-provider.ts +export interface SlackConfig { + botToken: string; + channelId: string; + username?: string; + iconEmoji?: string; +} + +export default class SlackNotificationProvider { + name = 'slack'; + private client?: any; + private config: SlackConfig; + + constructor(config: SlackConfig) { + this.config = config; + } + + async sendMessage(message: string): Promise { + if (!this.client) { + const { WebClient } = await import('@slack/web-api'); + this.client = new WebClient(this.config.botToken); + } + + try { + await this.client.chat.postMessage({ + channel: this.config.channelId, + text: message, + username: this.config.username || 'Nof1 Trader', + icon_emoji: this.config.iconEmoji || ':chart_with_upwards_trend:' + }); + } catch (error) { + console.error('Failed to send Slack message:', error); + throw error; + } + } + + isEnabled(): boolean { + return !!(this.config.botToken && this.config.channelId); + } +} +``` + +### 4. Message Formatter + +```typescript +// src/message-formatter.ts +export class SlackMessageFormatter { + static formatTradeMessage(data: any): string { + const { symbol, side, quantity, price, orderId, leverage, marginType } = data; + + const sideEmoji = side === 'BUY' ? ':chart_with_upwards_trend:' : ':chart_with_downwards_trend:'; + const sideText = side === 'BUY' ? 'LONG' : 'SHORT'; + + let message = `${sideEmoji} *Trade Executed*\n\n`; + message += `*Symbol:* ${symbol}\n`; + message += `*Position:* ${sideText}\n`; + message += `*Quantity:* ${quantity}\n`; + message += `*Price:* ${price}\n`; + message += `*Order ID:* \`${orderId}\`\n`; + + if (leverage) { + message += `*Leverage:* ${leverage}x\n`; + } + + if (marginType) { + const marginTypeText = marginType === 'ISOLATED' ? ':lock: Isolated' : ':arrows_counterclockwise: Cross'; + message += `*Margin Mode:* ${marginTypeText}\n`; + } + + return message; + } +} +``` + +## Usage Guide + +### 1. Installation + +```bash +npm install nof1-slack-plugin +``` + +### 2. Environment Variables Configuration + +```bash +# .env file +NOTIFICATION_PLUGINS=slack +SLACK_ENABLED=true +SLACK_PACKAGE=nof1-slack-plugin +SLACK_BOT_TOKEN=xoxb-your-bot-token +SLACK_CHANNEL_ID=C0123456789 +SLACK_USERNAME=Nof1 Trader +SLACK_ICON_EMOJI=:chart_with_upwards_trend: +``` + +### 3. Code Usage + +#### Automatic Loading +```typescript +import { TradingExecutor } from './services/trading-executor'; + +// Plugins are automatically loaded from environment variables +const executor = new TradingExecutor(); +``` + +#### Manual Addition +```typescript +import SlackPlugin from 'nof1-slack-plugin'; + +const slackProvider = new SlackPlugin({ + botToken: 'xoxb-your-token', + channelId: 'C0123456789' +}); + +executor.addNotificationProvider(slackProvider); +``` + +### 4. Testing + +```bash +# Test Slack plugin +npm start -- test-slack +``` + +## Configuration Options + +### Environment Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `NOTIFICATION_PLUGINS` | Comma-separated list of plugins to load | `slack,discord` | +| `{PLUGIN}_ENABLED` | Enable/disable specific plugin | `SLACK_ENABLED=true` | +| `{PLUGIN}_PACKAGE` | NPM package name for the plugin | `SLACK_PACKAGE=nof1-slack-plugin` | +| `{PLUGIN}_BOT_TOKEN` | Plugin-specific bot token | `SLACK_BOT_TOKEN=xoxb-xxx` | +| `{PLUGIN}_CHANNEL_ID` | Plugin-specific channel ID | `SLACK_CHANNEL_ID=C0123456789` | + +### Configuration File + +```json +{ + "notifications": { + "enabled": true, + "providers": { + "slack": { + "enabled": true, + "package": "nof1-slack-plugin", + "config": { + "botToken": "xoxb-xxx", + "channelId": "C0123456789", + "username": "Nof1 Trader", + "iconEmoji": ":chart_with_upwards_trend:" + } + }, + "discord": { + "enabled": false, + "package": "nof1-discord-plugin", + "config": { + "webhookUrl": "https://discord.com/api/webhooks/xxx" + } + } + } + } +} +``` + +## Implementation Steps + +### Phase 1: Core Architecture +1. Create NotificationProvider interface +2. Implement PluginLoader with runtime validation +3. Create NotificationManager for unified management +4. Extend ConfigManager for plugin support + +### Phase 2: Code Refactoring +1. Refactor TradingExecutor notification calls +2. Migrate TelegramService to plugin architecture +3. Update existing test cases +4. Ensure backward compatibility + +### Phase 3: Plugin Development +1. Create Slack plugin package structure +2. Implement Slack notification provider +3. Add Slack message formatting +4. Add configuration validation and error handling + +### Phase 4: Documentation and Examples +1. Update README with plugin system +2. Create plugin development guide +3. Provide configuration examples +4. Add troubleshooting section + +## Advantages + +- ✅ **Extensibility**: Support any notification channel through plugins +- ✅ **Loose Coupling**: Core trading logic separated from notification logic +- ✅ **Backward Compatibility**: Existing Telegram functionality remains unchanged +- ✅ **Fault Tolerance**: Notification failures don't affect trade execution +- ✅ **Easy to Use**: Dynamic loading with runtime validation +- ✅ **Flexible Configuration**: Support both environment variables and code configuration + +## Future Extensions + +### Potential Plugin Ideas + +1. **Discord Plugin**: Using Discord webhooks for notifications +2. **Email Plugin**: SMTP-based email notifications +3. **SMS Plugin**: SMS notifications via Twilio or similar services +4. **Webhook Plugin**: Generic HTTP webhook notifications +5. **Desktop Notification Plugin**: Native desktop notifications + +### Advanced Features + +1. **Message Templates**: Customizable message templates for different providers +2. **Rate Limiting**: Built-in rate limiting to prevent spam +3. **Message Queuing**: Asynchronous message queuing for better performance +4. **Notification Routing**: Route different message types to different providers +5. **Health Monitoring**: Built-in health checks and monitoring for all providers + +--- + +**Last Updated**: 2025-01-30 +**Version**: 1.0.0 +**Compatible with**: nof1-tracker v2.0.0+ \ No newline at end of file diff --git a/docs/plugin-notification-system-zh.md b/docs/plugin-notification-system-zh.md new file mode 100644 index 0000000..9fb4b77 --- /dev/null +++ b/docs/plugin-notification-system-zh.md @@ -0,0 +1,564 @@ +# 插件化通知系统 + +## 概述 + +本文档描述了 nof1-tracker 的插件化通知系统架构,将当前硬编码的 Telegram 通知改造为支持 Slack、Discord 等多种通知渠道的可扩展插件系统。 + +## 架构设计 + +### 核心组件 + +#### 1. 通知提供商接口 + +```typescript +// src/types/notification.ts +export interface TradeNotificationData { + symbol: string; + side: 'BUY' | 'SELL'; + quantity: string; + price: string; + orderId: string; + status: string; + leverage?: number; + marginType?: string; +} + +export interface StopOrderData { + type: 'take_profit' | 'stop_loss'; + symbol: string; + price: string; + orderId: string; +} + +export interface NotificationProvider { + name: string; + sendMessage(message: string): Promise; + isEnabled(): boolean; +} +``` + +#### 2. 插件加载器 + +插件加载器使用运行时接口验证动态加载通知插件,允许灵活的插件管理而无需复杂的依赖关系。 + +```typescript +// src/utils/plugin-loader.ts +export class PluginLoader { + static async loadNotificationPlugin( + packageName: string, + config?: any + ): Promise { + try { + const pluginModule = await import(packageName); + const ProviderClass = pluginModule.default || + pluginModule[`${packageName.split('-').pop()}Provider`]; + + if (!ProviderClass) { + throw new Error(`Plugin ${packageName} does not export a provider class`); + } + + const provider = new ProviderClass(config); + + // 运行时接口验证 + if (this.isValidProvider(provider)) { + console.log(`✅ Loaded notification plugin: ${packageName}`); + return provider; + } + + throw new Error('Plugin does not implement required NotificationProvider interface'); + } catch (error) { + console.warn(`❌ Failed to load plugin ${packageName}:`, error); + return null; + } + } + + private static isValidProvider(obj: any): obj is NotificationProvider { + return obj && + typeof obj.name === 'string' && + typeof obj.sendMessage === 'function' && + typeof obj.isEnabled === 'function'; + } +} +``` + +#### 3. 通知管理器 + +通知管理器管理多个通知提供商,并处理具有容错能力的消息广播。 + +```typescript +// src/services/notification-manager.ts +export class NotificationManager { + private providers: NotificationProvider[] = []; + + addProvider(provider: NotificationProvider): void { + this.providers.push(provider); + } + + async notifyTrade(data: TradeNotificationData): Promise { + const message = MessageFormatter.formatTradeMessage(data); + await this.broadcastMessage(message); + } + + private async broadcastMessage(message: string): Promise { + const enabledProviders = this.providers.filter(p => p.isEnabled()); + + // 并发发送,容错处理 + const promises = enabledProviders.map(async provider => { + try { + await provider.sendMessage(message); + } catch (error) { + console.error(`Failed to send notification via ${provider.name}:`, error); + } + }); + + await Promise.allSettled(promises); + } +} +``` + +#### 4. 消息格式化器 + +可跨不同通知提供商重用的集中式消息格式化逻辑。 + +```typescript +// src/utils/message-formatter.ts +export class MessageFormatter { + static formatTradeMessage(data: TradeNotificationData): string { + const { symbol, side, quantity, price, orderId, status, leverage, marginType } = data; + + const sideEmoji = side === 'BUY' ? '📈' : '📉'; + const sideText = side === 'BUY' ? 'LONG' : 'SHORT'; + + let message = `✅ Trade Executed\n\n`; + message += `${sideEmoji} ${sideText} ${symbol}\n`; + message += `💰 Quantity: ${quantity}\n`; + message += `💵 Price: ${price}\n`; + message += `🆔 Order ID: ${orderId}\n`; + message += `📊 Status: ${status}\n`; + + if (leverage) { + message += `⚡ Leverage: ${leverage}x\n`; + } + + if (marginType) { + const marginTypeText = marginType === 'ISOLATED' ? '🔒 Isolated' : '🔄 Cross'; + message += `${marginTypeText}\n`; + } + + return message; + } +} +``` + +## 主项目实现 + +### 1. 配置管理扩展 + +扩展现有的 ConfigManager 以支持多个通知提供商。 + +```typescript +// src/services/config-manager.ts +export interface TradingConfig { + // ... 现有配置 + notificationPlugins?: Record; + }>; +} + +export class ConfigManager { + loadFromEnvironment(): void { + // 加载通知插件配置 + const pluginConfigs = process.env.NOTIFICATION_PLUGINS; + if (pluginConfigs) { + const plugins = pluginConfigs.split(',').map(p => p.trim()); + this.config.notificationPlugins = {}; + + plugins.forEach(pluginName => { + const enabled = process.env[`${pluginName.toUpperCase()}_ENABLED`] === 'true'; + const packageName = process.env[`${pluginName.toUpperCase()}_PACKAGE`] || `nof1-${pluginName}-plugin`; + + const pluginConfig: Record = {}; + Object.keys(process.env).forEach(key => { + if (key.startsWith(`${pluginName.toUpperCase()}_`) && + !key.endsWith('_ENABLED') && + !key.endsWith('_PACKAGE')) { + const configKey = key.substring(pluginName.length + 1).toLowerCase(); + pluginConfig[configKey] = process.env[key]; + } + }); + + this.config.notificationPlugins[pluginName] = { + enabled, + package: packageName, + config: pluginConfig + }; + }); + } + } +} +``` + +### 2. TradingExecutor 重构 + +用统一的通知管理器替换硬编码的 Telegram 调用。 + +```typescript +// src/services/trading-executor.ts +export class TradingExecutor { + private notificationManager: NotificationManager; + + constructor(..., configManager?: ConfigManager) { + // ... 现有代码 + this.notificationManager = new NotificationManager(); + this.initializeNotifications(); + } + + private async initializeNotifications(): Promise { + // 加载内置 Telegram 支持 + const telegramConfig = this.configManager.getConfig().telegram; + if (telegramConfig.enabled) { + const telegramProvider = new TelegramService(telegramConfig.token); + this.notificationManager.addProvider(telegramProvider); + } + + // 加载外部插件 + const pluginConfigs = this.configManager.getConfig().notificationPlugins; + if (pluginConfigs) { + for (const [pluginName, pluginConfig] of Object.entries(pluginConfigs)) { + if (pluginConfig.enabled) { + const provider = await PluginLoader.loadNotificationPlugin( + pluginConfig.package, + pluginConfig.config + ); + if (provider) { + this.notificationManager.addProvider(provider); + } + } + } + } + } + + // 替换现有的 Telegram 通知调用 + private async sendTradeNotification(orderResponse: any, tradingPlan: TradingPlan): Promise { + await this.notificationManager.notifyTrade({ + symbol: orderResponse.symbol, + side: tradingPlan.side, + quantity: orderResponse.executedQty, + price: orderResponse.avgPrice || 'Market', + orderId: orderResponse.orderId.toString(), + status: orderResponse.status, + leverage: tradingPlan.leverage, + marginType: tradingPlan.marginType + }); + } +} +``` + +### 3. Telegram 服务适配器 + +调整现有的 TelegramService 以实现 NotificationProvider 接口。 + +```typescript +// src/services/telegram-service.ts +export class TelegramService implements NotificationProvider { + name = 'telegram'; + private bot: TelegramBot; + + constructor(token: string) { + this.bot = new TelegramBot(token, { polling: false }); + } + + // 实现 NotificationProvider 接口 + async sendMessage(message: string): Promise { + const configManager = new ConfigManager(); + configManager.loadFromEnvironment(); + const telegramConfig = configManager.getConfig().telegram; + + if (telegramConfig.chatId) { + await this.sendMessage(telegramConfig.chatId, message); + } else { + throw new Error('Telegram chatId not configured'); + } + } + + isEnabled(): boolean { + const configManager = new ConfigManager(); + configManager.loadFromEnvironment(); + const telegramConfig = configManager.getConfig().telegram; + return !!(telegramConfig.enabled && telegramConfig.token && telegramConfig.chatId); + } +} +``` + +## Slack 插件包设计 + +### 1. 包结构 + +``` +nof1-slack-plugin/ +├── package.json +├── README.md +├── tsconfig.json +├── src/ +│ ├── index.ts # 主入口 +│ ├── slack-provider.ts # Slack 提供商实现 +│ └── message-formatter.ts # Slack 消息格式化 +├── dist/ # 编译输出 +└── examples/ + └── usage-example.ts # 使用示例 +``` + +### 2. package.json + +```json +{ + "name": "nof1-slack-plugin", + "version": "1.0.0", + "description": "Slack notification plugin for nof1-tracker", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "test": "jest" + }, + "keywords": ["nof1", "slack", "notifications", "trading"], + "author": "Your Name", + "license": "MIT", + "dependencies": { + "@slack/web-api": "^6.9.0" + }, + "devDependencies": { + "@types/node": "^18.0.0", + "typescript": "^4.9.0" + } +} +``` + +### 3. Slack 提供商实现 + +```typescript +// src/slack-provider.ts +export interface SlackConfig { + botToken: string; + channelId: string; + username?: string; + iconEmoji?: string; +} + +export default class SlackNotificationProvider { + name = 'slack'; + private client?: any; + private config: SlackConfig; + + constructor(config: SlackConfig) { + this.config = config; + } + + async sendMessage(message: string): Promise { + if (!this.client) { + const { WebClient } = await import('@slack/web-api'); + this.client = new WebClient(this.config.botToken); + } + + try { + await this.client.chat.postMessage({ + channel: this.config.channelId, + text: message, + username: this.config.username || 'Nof1 Trader', + icon_emoji: this.config.iconEmoji || ':chart_with_upwards_trend:' + }); + } catch (error) { + console.error('Failed to send Slack message:', error); + throw error; + } + } + + isEnabled(): boolean { + return !!(this.config.botToken && this.config.channelId); + } +} +``` + +### 4. 消息格式化器 + +```typescript +// src/message-formatter.ts +export class SlackMessageFormatter { + static formatTradeMessage(data: any): string { + const { symbol, side, quantity, price, orderId, leverage, marginType } = data; + + const sideEmoji = side === 'BUY' ? ':chart_with_upwards_trend:' : ':chart_with_downwards_trend:'; + const sideText = side === 'BUY' ? 'LONG' : 'SHORT'; + + let message = `${sideEmoji} *Trade Executed*\n\n`; + message += `*Symbol:* ${symbol}\n`; + message += `*Position:* ${sideText}\n`; + message += `*Quantity:* ${quantity}\n`; + message += `*Price:* ${price}\n`; + message += `*Order ID:* \`${orderId}\`\n`; + + if (leverage) { + message += `*Leverage:* ${leverage}x\n`; + } + + if (marginType) { + const marginTypeText = marginType === 'ISOLATED' ? ':lock: Isolated' : ':arrows_counterclockwise: Cross'; + message += `*Margin Mode:* ${marginTypeText}\n`; + } + + return message; + } +} +``` + +## 使用指南 + +### 1. 安装 + +```bash +npm install nof1-slack-plugin +``` + +### 2. 环境变量配置 + +```bash +# .env 文件 +NOTIFICATION_PLUGINS=slack +SLACK_ENABLED=true +SLACK_PACKAGE=nof1-slack-plugin +SLACK_BOT_TOKEN=xoxb-your-bot-token +SLACK_CHANNEL_ID=C0123456789 +SLACK_USERNAME=Nof1 Trader +SLACK_ICON_EMOJI=:chart_with_upwards_trend: +``` + +### 3. 代码使用 + +#### 自动加载 +```typescript +import { TradingExecutor } from './services/trading-executor'; + +// 插件会自动从环境变量加载 +const executor = new TradingExecutor(); +``` + +#### 手动添加 +```typescript +import SlackPlugin from 'nof1-slack-plugin'; + +const slackProvider = new SlackPlugin({ + botToken: 'xoxb-your-token', + channelId: 'C0123456789' +}); + +executor.addNotificationProvider(slackProvider); +``` + +### 4. 测试 + +```bash +# 测试 Slack 插件 +npm start -- test-slack +``` + +## 配置选项 + +### 环境变量 + +| 变量 | 描述 | 示例 | +|------|------|------| +| `NOTIFICATION_PLUGINS` | 要加载的插件列表(逗号分隔) | `slack,discord` | +| `{PLUGIN}_ENABLED` | 启用/禁用特定插件 | `SLACK_ENABLED=true` | +| `{PLUGIN}_PACKAGE` | 插件的 NPM 包名 | `SLACK_PACKAGE=nof1-slack-plugin` | +| `{PLUGIN}_BOT_TOKEN` | 插件特定的机器人令牌 | `SLACK_BOT_TOKEN=xoxb-xxx` | +| `{PLUGIN}_CHANNEL_ID` | 插件特定的频道 ID | `SLACK_CHANNEL_ID=C0123456789` | + +### 配置文件 + +```json +{ + "notifications": { + "enabled": true, + "providers": { + "slack": { + "enabled": true, + "package": "nof1-slack-plugin", + "config": { + "botToken": "xoxb-xxx", + "channelId": "C0123456789", + "username": "Nof1 Trader", + "iconEmoji": ":chart_with_upwards_trend:" + } + }, + "discord": { + "enabled": false, + "package": "nof1-discord-plugin", + "config": { + "webhookUrl": "https://discord.com/api/webhooks/xxx" + } + } + } + } +} +``` + +## 实施步骤 + +### 第一阶段:核心架构 +1. 创建 NotificationProvider 接口 +2. 实现运行时验证的 PluginLoader +3. 创建统一管理的 NotificationManager +4. 扩展 ConfigManager 支持插件 + +### 第二阶段:代码重构 +1. 重构 TradingExecutor 通知调用 +2. 将 TelegramService 迁移到插件架构 +3. 更新现有测试用例 +4. 确保向后兼容性 + +### 第三阶段:插件开发 +1. 创建 Slack 插件包结构 +2. 实现 Slack 通知提供商 +3. 添加 Slack 消息格式化 +4. 添加配置验证和错误处理 + +### 第四阶段:文档和示例 +1. 用插件系统更新 README +2. 创建插件开发指南 +3. 提供配置示例 +4. 添加故障排除部分 + +## 优势 + +- ✅ **可扩展性**: 通过插件支持任何通知渠道 +- ✅ **松耦合**: 核心交易逻辑与通知逻辑分离 +- ✅ **向后兼容**: 现有 Telegram 功能保持不变 +- ✅ **容错性**: 通知失败不影响交易执行 +- ✅ **简单易用**: 动态加载,运行时验证 +- ✅ **灵活配置**: 支持环境变量和代码配置 + +## 未来扩展 + +### 潜在插件想法 + +1. **Discord 插件**: 使用 Discord webhooks 进行通知 +2. **邮件插件**: 基于 SMTP 的邮件通知 +3. **短信插件**: 通过 Twilio 或类似服务发送短信通知 +4. **Webhook 插件**: 通用 HTTP webhook 通知 +5. **桌面通知插件**: 原生桌面通知 + +### 高级功能 + +1. **消息模板**: 为不同提供商提供可定制的消息模板 +2. **速率限制**: 内置速率限制防止垃圾信息 +3. **消息队列**: 异步消息队列以获得更好的性能 +4. **通知路由**: 将不同类型的消息路由到不同的提供商 +5. **健康监控**: 对所有提供商进行内置健康检查和监控 + +--- + +**最后更新**: 2025-01-30 +**版本**: 1.0.0 +**兼容**: nof1-tracker v2.0.0+ \ No newline at end of file From c9ee52ab0547b6eb0d2218f289ed4905c87129cb Mon Sep 17 00:00:00 2001 From: M3R1ttt Date: Sun, 2 Nov 2025 21:02:37 +0300 Subject: [PATCH 2/2] feat: notification service added --- .env.example | 25 +- README_EN.md | 76 +--- package-lock.json | 11 +- package.json | 1 + src/__tests__/commands/telegram.test.ts | 230 ------------ .../services/telegram-service.test.ts | 352 ------------------ src/commands/index.ts | 3 +- src/commands/telegram.ts | 36 -- src/index.ts | 12 - src/services/telegram-service.ts | 76 ---- src/services/trading-executor.ts | 75 ++-- src/utils/notification-service.ts | 41 ++ 12 files changed, 113 insertions(+), 825 deletions(-) delete mode 100644 src/__tests__/commands/telegram.test.ts delete mode 100644 src/__tests__/services/telegram-service.test.ts delete mode 100644 src/commands/telegram.ts delete mode 100644 src/services/telegram-service.ts create mode 100644 src/utils/notification-service.ts diff --git a/.env.example b/.env.example index 50b51fe..92ac43b 100644 --- a/.env.example +++ b/.env.example @@ -32,4 +32,27 @@ LOG_LEVEL=INFO # 获取方式:https://core.telegram.org/bots#6-botfather TELEGRAM_API_TOKEN= TELEGRAM_CHAT_ID= -TELEGRAM_ENABLED=false \ No newline at end of file +TELEGRAM_ENABLED=false + + +# Tracker-Notification System Configuration +# 📱 Enhanced notification system for multiple platforms +# ======================================= + +# Telegram Configuration (Enhanced) +# Bot Token: Get from @BotFather on Telegram +# Chat ID: Send message to your bot and check updates via getUpdates API +TELEGRAM_BOT_TOKEN=telegram_token +TELEGRAM_CHAT_ID=telegram_chat_id + +# Slack Configuration (Optional) +# Bot Token: Create Slack App at https://api.slack.com/apps +# Channel ID: Right-click channel → "Copy link" → extract ID from URL +SLACK_BOT_TOKEN=xoxb-1234 +SLACK_CHANNEL_ID="#new-channel" + + +# Notification Providers Selection +# Set to 'true' to enable each provider +NOTIFICATION_TELEGRAM_ENABLED=false +NOTIFICATION_SLACK_ENABLED=false \ No newline at end of file diff --git a/README_EN.md b/README_EN.md index 5563a1b..a94d39a 100644 --- a/README_EN.md +++ b/README_EN.md @@ -43,9 +43,6 @@ cp .env.example .env # 3. View available AI Agents npm start -- agents -# 4. Test Telegram notifications (optional) -npm start -- telegram-test - # 5. Start copy trading (risk-only mode, no real trades) npm start -- follow deepseek-chat-v3.1 --risk-only @@ -66,7 +63,7 @@ npm start -- profit - **⚡ Futures Trading**: Full support for Binance USDT perpetual futures, 1x-125x leverage - **📈 Profit Analysis**: Accurate profit analysis based on real trading data (including fee statistics) - **🛡️ Risk Control**: Support `--risk-only` mode for observation without execution -- **📱 Telegram Notifications**: Real-time Telegram notifications for trade executions and stop-loss/take-profit events +- **📱 Notifications Service**: Real-time notifications for trade executions and stop-loss/take-profit events ## 📊 Live Trading Dashboard @@ -76,9 +73,9 @@ Real-time view of deepseek-chat-v3.1 AI Agent's trading performance, positions, Dashboard: https://github.com/terryso/nof1-tracker-dashboard -## 📱 Telegram Notifications +## 📱 Notification Service -Enable Telegram notifications to receive real-time alerts about your trading activities: +Enable notifications to receive real-time alerts about your trading activities: ### Features @@ -157,35 +154,7 @@ This system uses **Binance Futures Trading API**, permissions must be configured BINANCE_API_SECRET=testnet_secret_key ``` -### 2. Telegram Notification Setup (Optional) - -Set up Telegram notifications to receive real-time trading signals and alerts: - -1. **Create a Telegram Bot**: - - Open Telegram and search for [@BotFather](https://t.me/BotFather) - - Send `/newbot` command and follow the instructions - - Save the bot token you receive - -2. **Get Your Chat ID**: - - Search for your bot in Telegram - - Send any message to your bot - - Visit `https://api.telegram.org/bot/getUpdates` - - Look for `"chat":{"id":}` - -3. **Configure Environment Variables**: - ```env - # Telegram Configuration (Optional) - TELEGRAM_ENABLED=true - TELEGRAM_API_TOKEN=your_telegram_bot_token - TELEGRAM_CHAT_ID=your_telegram_chat_id - ``` - -4. **Test Telegram Connection**: - ```bash - npm start -- telegram-test - ``` - -### 3. Environment Variables +### 2. Environment Variables ```env # Binance API Configuration - Must support futures trading @@ -196,10 +165,9 @@ BINANCE_TESTNET=true # true=testnet, false=mainnet # Other Configuration Options LOG_LEVEL=INFO # Log level -# Telegram Configuration (Optional) -TELEGRAM_ENABLED=true -TELEGRAM_API_TOKEN=your_telegram_bot_token -TELEGRAM_CHAT_ID=your_telegram_chat_id +# Notification Configuration (Optional) +NOTIFICATION_TELEGRAM_ENABLED=true +NOTIFICATION_SLACK_ENABLED=true ``` ## 📖 Usage @@ -329,11 +297,6 @@ npm start -- profit --exclude-unrealized npm start -- status ``` -#### 5. Telegram Notification Test -```bash -# Test Telegram bot connection and send test message -npm start -- telegram-test -``` ### Copy Trading Strategy @@ -436,9 +399,6 @@ npm start -- status # 2. View available agents npm start -- agents -# 3. Test Telegram notifications (if configured) -npm start -- telegram-test - # 4. Risk control mode test npm start -- follow buynhold_btc --risk-only @@ -496,7 +456,6 @@ src/ │ ├── follow.ts # Copy trade command (core) │ ├── profit.ts # Profit statistics analysis │ ├── status.ts # System status check -│ └── telegram.ts # Telegram notification test ├── services/ # Core services │ ├── api-client.ts # Nof1 API client │ ├── binance-service.ts # Binance API integration @@ -506,7 +465,6 @@ src/ │ ├── trade-history-service.ts # Trade history service │ ├── order-history-manager.ts # Order history management │ ├── futures-capital-manager.ts # Futures capital management -│ └── telegram-service.ts # Telegram notification service ├── scripts/ │ └── analyze-api.ts # API analysis engine (copy trading strategy) ├── types/ # TypeScript type definitions @@ -525,7 +483,7 @@ User Command → follow handler → ApiAnalyzer analyzes agent signals ↓ BinanceService → Binance API → Trade completed ↓ - TelegramService sends notification (if enabled) + Notification Service sends notification (if enabled) Profit Analysis Flow: User Command → profit handler → TradeHistoryService fetches historical trades @@ -536,14 +494,6 @@ User Command → profit handler → TradeHistoryService fetches historical trade ↓ Output results (table/JSON format) -Telegram Notification Flow: -Trading Executor → Trade/Order event - ↓ - TelegramService.formatTradeMessage() - ↓ - Send to Telegram API - ↓ - User receives notification ``` ## ⚠️ Important Notes @@ -597,16 +547,6 @@ Error: Invalid API Key - Confirm API key has not expired - Verify complete key is copied (no extra spaces) -**5. Telegram Notification Issues** -``` -Error: Failed to send Telegram message -``` -- ✅ Check if `TELEGRAM_ENABLED=true` in `.env` file -- ✅ Verify Telegram bot token is correct (from @BotFather) -- ✅ Verify chat ID is correct (get from bot API) -- ✅ Test with `npm start -- telegram-test` -- ✅ Ensure bot has not been blocked or deleted -- ✅ Check internet connection for Telegram API access ## 🔧 Development diff --git a/package-lock.json b/package-lock.json index 381a3d8..41bb32d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nof1-tracker", - "version": "1.0.3", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nof1-tracker", - "version": "1.0.3", + "version": "1.1.1", "license": "MIT", "dependencies": { "@types/fs-extra": "^11.0.4", @@ -18,6 +18,7 @@ "fs-extra": "^11.3.2", "node-telegram-bot-api": "^0.66.0", "querystring": "^0.2.1", + "tracker-notification": "^1.0.0", "winston": "^3.11.0" }, "bin": { @@ -7322,6 +7323,12 @@ "node": ">=16" } }, + "node_modules/tracker-notification": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tracker-notification/-/tracker-notification-1.0.0.tgz", + "integrity": "sha512-QvHtMfNseJpyaRVDxLL781XI/4x/vmTPG8Q1NPwc5PhRAOf/stVQtKqFrOIDc2iaKak784m/E6fgyWoVUOdPuA==", + "license": "MIT" + }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", diff --git a/package.json b/package.json index 1a558f5..3822e51 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "fs-extra": "^11.3.2", "node-telegram-bot-api": "^0.66.0", "querystring": "^0.2.1", + "tracker-notification": "^1.0.0", "winston": "^3.11.0" }, "devDependencies": { diff --git a/src/__tests__/commands/telegram.test.ts b/src/__tests__/commands/telegram.test.ts deleted file mode 100644 index 9aca54b..0000000 --- a/src/__tests__/commands/telegram.test.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { handleTelegramCommand } from '../../commands/telegram'; -import { ConfigManager } from '../../services/config-manager'; -import { TelegramService } from '../../services/telegram-service'; - -// Mock dependencies -jest.mock('../../services/config-manager'); -jest.mock('../../services/telegram-service'); - -describe('Telegram Command Handler', () => { - let mockConfigManager: jest.Mocked; - let mockTelegramService: jest.Mocked; - let consoleLogSpy: jest.SpyInstance; - let consoleErrorSpy: jest.SpyInstance; - - beforeEach(() => { - // Reset all mocks - jest.clearAllMocks(); - - // Mock console methods - consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(); - consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); - - // Mock ConfigManager - mockConfigManager = new ConfigManager() as jest.Mocked; - mockConfigManager.loadFromEnvironment = jest.fn(); - mockConfigManager.getConfig = jest.fn().mockReturnValue({ - defaultPriceTolerance: 1.0, - symbolTolerances: {}, - telegram: { - enabled: false, - token: '', - chatId: '', - }, - }); - - (ConfigManager as jest.MockedClass).mockImplementation(() => mockConfigManager); - - // Mock TelegramService - mockTelegramService = { - sendMessage: jest.fn().mockResolvedValue(undefined), - } as any; - - (TelegramService as jest.MockedClass).mockImplementation(() => mockTelegramService); - }); - - afterEach(() => { - consoleLogSpy.mockRestore(); - consoleErrorSpy.mockRestore(); - }); - - describe('when telegram is disabled', () => { - it('should log error and not send message', async () => { - mockConfigManager.getConfig = jest.fn().mockReturnValue({ - defaultPriceTolerance: 1.0, - symbolTolerances: {}, - telegram: { - enabled: false, - token: 'test-token', - chatId: '123', - }, - }); - - await handleTelegramCommand({} as any); - - expect(consoleLogSpy).toHaveBeenCalledWith('❌ Telegram notifications are not enabled in your configuration. Set TELEGRAM_ENABLED=true in your .env file.'); - expect(TelegramService).not.toHaveBeenCalled(); - }); - }); - - describe('when token is missing', () => { - it('should log error and not send message', async () => { - mockConfigManager.getConfig = jest.fn().mockReturnValue({ - defaultPriceTolerance: 1.0, - symbolTolerances: {}, - telegram: { - enabled: true, - token: '', - chatId: '123', - }, - }); - - await handleTelegramCommand({} as any); - - expect(consoleLogSpy).toHaveBeenCalledWith('❌ Telegram API Token is not set. Please set TELEGRAM_API_TOKEN in your .env file.'); - expect(TelegramService).not.toHaveBeenCalled(); - }); - }); - - describe('when chatId is missing', () => { - it('should log error and not send message', async () => { - mockConfigManager.getConfig = jest.fn().mockReturnValue({ - defaultPriceTolerance: 1.0, - symbolTolerances: {}, - telegram: { - enabled: true, - token: 'test-token', - chatId: '', - }, - }); - - await handleTelegramCommand({} as any); - - expect(consoleLogSpy).toHaveBeenCalledWith('❌ Telegram Chat ID is not set. Please set TELEGRAM_CHAT_ID in your .env file.'); - expect(TelegramService).not.toHaveBeenCalled(); - }); - }); - - describe('when configuration is valid', () => { - it('should send test message successfully', async () => { - mockConfigManager.getConfig = jest.fn().mockReturnValue({ - defaultPriceTolerance: 1.0, - symbolTolerances: {}, - telegram: { - enabled: true, - token: 'test-token-123', - chatId: '123456789', - }, - }); - - await handleTelegramCommand({} as any); - - expect(consoleLogSpy).toHaveBeenCalledWith('🚀 Attempting to send a test Telegram message...'); - expect(ConfigManager).toHaveBeenCalled(); - expect(mockConfigManager.loadFromEnvironment).toHaveBeenCalled(); - expect(TelegramService).toHaveBeenCalledWith('test-token-123'); - expect(mockTelegramService.sendMessage).toHaveBeenCalledWith( - '123456789', - '🤖 Nof1 Tracker: This is a test message from your bot!' - ); - expect(consoleLogSpy).toHaveBeenCalledWith('✅ Test Telegram message sent successfully!'); - }); - - it('should handle send message failure', async () => { - mockConfigManager.getConfig = jest.fn().mockReturnValue({ - defaultPriceTolerance: 1.0, - symbolTolerances: {}, - telegram: { - enabled: true, - token: 'test-token-123', - chatId: '123456789', - }, - }); - - const error = new Error('Network error'); - mockTelegramService.sendMessage.mockRejectedValueOnce(error); - - await handleTelegramCommand({} as any); - - expect(consoleErrorSpy).toHaveBeenCalledWith('❌ Failed to send test Telegram message:', 'Network error'); - }); - - it('should handle different error types', async () => { - mockConfigManager.getConfig = jest.fn().mockReturnValue({ - defaultPriceTolerance: 1.0, - symbolTolerances: {}, - telegram: { - enabled: true, - token: 'test-token-123', - chatId: '123456789', - }, - }); - - mockTelegramService.sendMessage.mockRejectedValueOnce('String error'); - - await handleTelegramCommand({} as any); - - expect(consoleErrorSpy).toHaveBeenCalledWith('❌ Failed to send test Telegram message:', 'String error'); - }); - }); - - describe('config loading', () => { - it('should load configuration from environment', async () => { - mockConfigManager.getConfig = jest.fn().mockReturnValue({ - defaultPriceTolerance: 1.0, - symbolTolerances: {}, - telegram: { - enabled: true, - token: 'test-token', - chatId: '123', - }, - }); - - await handleTelegramCommand({} as any); - - expect(mockConfigManager.loadFromEnvironment).toHaveBeenCalled(); - expect(mockConfigManager.getConfig).toHaveBeenCalled(); - }); - }); - - describe('telegram service initialization', () => { - it('should create TelegramService with correct token', async () => { - const testToken = 'my-special-token'; - mockConfigManager.getConfig = jest.fn().mockReturnValue({ - defaultPriceTolerance: 1.0, - symbolTolerances: {}, - telegram: { - enabled: true, - token: testToken, - chatId: '123', - }, - }); - - await handleTelegramCommand({} as any); - - expect(TelegramService).toHaveBeenCalledWith(testToken); - }); - }); - - describe('message content', () => { - it('should send the correct test message', async () => { - mockConfigManager.getConfig = jest.fn().mockReturnValue({ - defaultPriceTolerance: 1.0, - symbolTolerances: {}, - telegram: { - enabled: true, - token: 'test-token', - chatId: 'chat123', - }, - }); - - await handleTelegramCommand({} as any); - - expect(mockTelegramService.sendMessage).toHaveBeenCalledWith( - 'chat123', - '🤖 Nof1 Tracker: This is a test message from your bot!' - ); - }); - }); -}); - diff --git a/src/__tests__/services/telegram-service.test.ts b/src/__tests__/services/telegram-service.test.ts deleted file mode 100644 index 90274c0..0000000 --- a/src/__tests__/services/telegram-service.test.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { TelegramService, TradeNotificationData } from '../../services/telegram-service'; -import TelegramBot from 'node-telegram-bot-api'; - -// Mock node-telegram-bot-api -jest.mock('node-telegram-bot-api'); - -describe('TelegramService', () => { - let telegramService: TelegramService; - let mockBot: jest.Mocked; - const mockToken = 'test-token-123'; - - beforeEach(() => { - // Reset all mocks - jest.clearAllMocks(); - - // Create a mock bot instance - mockBot = { - sendMessage: jest.fn().mockResolvedValue({ message_id: 1 }), - } as any; - - // Mock the TelegramBot constructor - (TelegramBot as jest.MockedClass).mockImplementation(() => mockBot); - - // Create the service instance - telegramService = new TelegramService(mockToken); - }); - - describe('Constructor', () => { - it('should create TelegramService instance with token', () => { - expect(telegramService).toBeInstanceOf(TelegramService); - expect(TelegramBot).toHaveBeenCalledWith(mockToken, { polling: false }); - }); - - it('should initialize TelegramBot with correct options', () => { - new TelegramService('another-token'); - expect(TelegramBot).toHaveBeenCalledWith('another-token', { polling: false }); - }); - }); - - describe('sendMessage', () => { - const chatId = '123456789'; - const testMessage = 'Test message'; - - it('should send message successfully', async () => { - await telegramService.sendMessage(chatId, testMessage); - - expect(mockBot.sendMessage).toHaveBeenCalledWith(chatId, testMessage, { parse_mode: 'HTML' }); - }); - - it('should throw error when message sending fails', async () => { - const error = new Error('Network error'); - mockBot.sendMessage.mockRejectedValueOnce(error); - - await expect(telegramService.sendMessage(chatId, testMessage)).rejects.toThrow('Network error'); - expect(mockBot.sendMessage).toHaveBeenCalledWith(chatId, testMessage, { parse_mode: 'HTML' }); - }); - - it('should handle different chat IDs', async () => { - const chatId1 = '111111111'; - const chatId2 = '222222222'; - - await telegramService.sendMessage(chatId1, testMessage); - await telegramService.sendMessage(chatId2, testMessage); - - expect(mockBot.sendMessage).toHaveBeenCalledWith(chatId1, testMessage, { parse_mode: 'HTML' }); - expect(mockBot.sendMessage).toHaveBeenCalledWith(chatId2, testMessage, { parse_mode: 'HTML' }); - expect(mockBot.sendMessage).toHaveBeenCalledTimes(2); - }); - - it('should send different message types', async () => { - const message1 = 'First message'; - const message2 = 'Second message'; - - await telegramService.sendMessage(chatId, message1); - await telegramService.sendMessage(chatId, message2); - - expect(mockBot.sendMessage).toHaveBeenCalledWith(chatId, message1, { parse_mode: 'HTML' }); - expect(mockBot.sendMessage).toHaveBeenCalledWith(chatId, message2, { parse_mode: 'HTML' }); - }); - }); - - describe('formatTradeMessage', () => { - it('should format BUY trade message correctly', () => { - const data: TradeNotificationData = { - symbol: 'BTCUSDT', - side: 'BUY', - quantity: '1.5', - price: '50000.00', - orderId: '123456', - status: 'FILLED', - leverage: 10, - marginType: 'ISOLATED' - }; - - const message = telegramService.formatTradeMessage(data); - - expect(message).toContain('✅'); - expect(message).toContain('Trade Executed'); - expect(message).toContain('📈'); - expect(message).toContain('LONG'); - expect(message).toContain('BTCUSDT'); - expect(message).toContain('💰'); - expect(message).toContain('Quantity: 1.5'); - expect(message).toContain('💵'); - expect(message).toContain('Price: 50000.00'); - expect(message).toContain('🆔'); - expect(message).toContain('Order ID: 123456'); - expect(message).toContain('📊'); - expect(message).toContain('Status: FILLED'); - expect(message).toContain('⚡'); - expect(message).toContain('Leverage: 10x'); - expect(message).toContain('🔒 Isolated'); - }); - - it('should format SELL trade message correctly', () => { - const data: TradeNotificationData = { - symbol: 'ETHUSDT', - side: 'SELL', - quantity: '2.0', - price: '3000.00', - orderId: '789012', - status: 'FILLED' - }; - - const message = telegramService.formatTradeMessage(data); - - expect(message).toContain('✅'); - expect(message).toContain('Trade Executed'); - expect(message).toContain('📉'); - expect(message).toContain('SHORT'); - expect(message).toContain('ETHUSDT'); - expect(message).toContain('💰'); - expect(message).toContain('Quantity: 2.0'); - expect(message).toContain('💵'); - expect(message).toContain('Price: 3000.00'); - expect(message).toContain('🆔'); - expect(message).toContain('Order ID: 789012'); - expect(message).toContain('📊'); - expect(message).toContain('Status: FILLED'); - }); - - it('should include leverage when provided', () => { - const data: TradeNotificationData = { - symbol: 'BTCUSDT', - side: 'BUY', - quantity: '1.0', - price: '50000.00', - orderId: '123', - status: 'FILLED', - leverage: 25 - }; - - const message = telegramService.formatTradeMessage(data); - - expect(message).toContain('⚡'); - expect(message).toContain('Leverage: 25x'); - }); - - it('should not include leverage when not provided', () => { - const data: TradeNotificationData = { - symbol: 'BTCUSDT', - side: 'BUY', - quantity: '1.0', - price: '50000.00', - orderId: '123', - status: 'FILLED' - }; - - const message = telegramService.formatTradeMessage(data); - - expect(message).not.toContain('⚡'); - expect(message).not.toContain('Leverage'); - }); - - it('should include isolated margin type', () => { - const data: TradeNotificationData = { - symbol: 'BTCUSDT', - side: 'BUY', - quantity: '1.0', - price: '50000.00', - orderId: '123', - status: 'FILLED', - marginType: 'ISOLATED' - }; - - const message = telegramService.formatTradeMessage(data); - - expect(message).toContain('🔒 Isolated'); - }); - - it('should include cross margin type', () => { - const data: TradeNotificationData = { - symbol: 'BTCUSDT', - side: 'BUY', - quantity: '1.0', - price: '50000.00', - orderId: '123', - status: 'FILLED', - marginType: 'CROSSED' - }; - - const message = telegramService.formatTradeMessage(data); - - expect(message).toContain('🔄 Cross'); - }); - - it('should not include margin type when not provided', () => { - const data: TradeNotificationData = { - symbol: 'BTCUSDT', - side: 'BUY', - quantity: '1.0', - price: '50000.00', - orderId: '123', - status: 'FILLED' - }; - - const message = telegramService.formatTradeMessage(data); - - expect(message).not.toContain('🔒'); - expect(message).not.toContain('🔄'); - }); - - it('should handle all fields with different values', () => { - const data: TradeNotificationData = { - symbol: 'ADAUSDT', - side: 'SELL', - quantity: '1000', - price: '0.50', - orderId: '999888', - status: 'PARTIALLY_FILLED', - leverage: 5, - marginType: 'CROSSED' - }; - - const message = telegramService.formatTradeMessage(data); - - expect(message).toContain('ADAUSDT'); - expect(message).toContain('SHORT'); - expect(message).toContain('Quantity: 1000'); - expect(message).toContain('Price: 0.50'); - expect(message).toContain('Order ID: 999888'); - expect(message).toContain('Status: PARTIALLY_FILLED'); - expect(message).toContain('Leverage: 5x'); - expect(message).toContain('🔄 Cross'); - }); - }); - - describe('formatStopOrderMessage', () => { - it('should format take profit order message correctly', () => { - const message = telegramService.formatStopOrderMessage( - 'take_profit', - 'BTCUSDT', - '55000.00', - 'tp123' - ); - - expect(message).toContain('🎯'); - expect(message).toContain('Take Profit Order Set'); - expect(message).toContain('📊'); - expect(message).toContain('Symbol: BTCUSDT'); - expect(message).toContain('💵'); - expect(message).toContain('Price: 55000.00'); - expect(message).toContain('🆔'); - expect(message).toContain('Order ID: tp123'); - }); - - it('should format stop loss order message correctly', () => { - const message = telegramService.formatStopOrderMessage( - 'stop_loss', - 'ETHUSDT', - '2800.00', - 'sl456' - ); - - expect(message).toContain('🛡️'); - expect(message).toContain('Stop Loss Order Set'); - expect(message).toContain('📊'); - expect(message).toContain('Symbol: ETHUSDT'); - expect(message).toContain('💵'); - expect(message).toContain('Price: 2800.00'); - expect(message).toContain('🆔'); - expect(message).toContain('Order ID: sl456'); - }); - - it('should handle different symbols and prices', () => { - const message = telegramService.formatStopOrderMessage( - 'take_profit', - 'ADAUSDT', - '0.75', - 'tp789' - ); - - expect(message).toContain('ADAUSDT'); - expect(message).toContain('Price: 0.75'); - expect(message).toContain('Order ID: tp789'); - }); - }); - - describe('Integration: sendMessage with formatTradeMessage', () => { - it('should send a formatted trade message', async () => { - const chatId = '123456789'; - const data: TradeNotificationData = { - symbol: 'BTCUSDT', - side: 'BUY', - quantity: '1.0', - price: '50000.00', - orderId: '123', - status: 'FILLED', - leverage: 10, - marginType: 'ISOLATED' - }; - - const formattedMessage = telegramService.formatTradeMessage(data); - await telegramService.sendMessage(chatId, formattedMessage); - - expect(mockBot.sendMessage).toHaveBeenCalledWith(chatId, formattedMessage, { parse_mode: 'HTML' }); - expect(formattedMessage).toContain('Trade Executed'); - expect(formattedMessage).toContain('BTCUSDT'); - }); - }); - - describe('Error handling in console', () => { - let consoleErrorSpy: jest.SpyInstance; - let consoleLogSpy: jest.SpyInstance; - - beforeEach(() => { - consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); - consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(); - }); - - afterEach(() => { - consoleErrorSpy.mockRestore(); - consoleLogSpy.mockRestore(); - }); - - it('should log error when sending message fails', async () => { - const error = new Error('Connection failed'); - mockBot.sendMessage.mockRejectedValueOnce(error); - - await expect(telegramService.sendMessage('123', 'test')).rejects.toThrow('Connection failed'); - - expect(consoleErrorSpy).toHaveBeenCalledWith('Error sending message to Telegram:', error); - }); - - it('should log when sending message starts', async () => { - await telegramService.sendMessage('123', 'test'); - - expect(consoleLogSpy).toHaveBeenCalledWith('Sending message to Telegram...'); - }); - }); -}); - diff --git a/src/commands/index.ts b/src/commands/index.ts index 336a92b..b9301e6 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -4,5 +4,4 @@ export { handleAgentsCommand } from './agents'; export { handleFollowCommand } from './follow'; export { handleStatusCommand } from './status'; -export { handleProfitCommand, ProfitCommandOptions } from './profit'; -export { handleTelegramCommand } from './telegram'; \ No newline at end of file +export { handleProfitCommand, ProfitCommandOptions } from './profit'; \ No newline at end of file diff --git a/src/commands/telegram.ts b/src/commands/telegram.ts deleted file mode 100644 index 96d7b99..0000000 --- a/src/commands/telegram.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { CommandOptions } from '../types/command'; -import { ConfigManager } from '../services/config-manager'; -import { TelegramService } from '../services/telegram-service'; - -export async function handleTelegramCommand(options: CommandOptions): Promise { - console.log('🚀 Attempting to send a test Telegram message...'); - - const configManager = new ConfigManager(); - configManager.loadFromEnvironment(); - - const telegramConfig = configManager.getConfig().telegram; - - if (!telegramConfig.enabled) { - console.log('❌ Telegram notifications are not enabled in your configuration. Set TELEGRAM_ENABLED=true in your .env file.'); - return; - } - - if (!telegramConfig.token) { - console.log('❌ Telegram API Token is not set. Please set TELEGRAM_API_TOKEN in your .env file.'); - return; - } - - if (!telegramConfig.chatId) { - console.log('❌ Telegram Chat ID is not set. Please set TELEGRAM_CHAT_ID in your .env file.'); - return; - } - - try { - const telegramService = new TelegramService(telegramConfig.token); - const testMessage = '🤖 Nof1 Tracker: This is a test message from your bot!'; - await telegramService.sendMessage(telegramConfig.chatId, testMessage); - console.log('✅ Test Telegram message sent successfully!'); - } catch (error) { - console.error('❌ Failed to send test Telegram message:', error instanceof Error ? error.message : error); - } -} diff --git a/src/index.ts b/src/index.ts index 8d5d966..d6a5888 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,6 @@ import { handleStatusCommand, handleProfitCommand, ProfitCommandOptions, - handleTelegramCommand } from './commands'; import { handleError, getVersion } from './utils/command-helpers'; @@ -62,17 +61,6 @@ program } }); -program - .command('telegram-test') - .description('Send a test Telegram message') - .action(async (options) => { - try { - await handleTelegramCommand(options); - } catch (error) { - handleError(error, 'Failed to send test Telegram message'); - } - }); - // Status command program .command('status') diff --git a/src/services/telegram-service.ts b/src/services/telegram-service.ts deleted file mode 100644 index b87c6fc..0000000 --- a/src/services/telegram-service.ts +++ /dev/null @@ -1,76 +0,0 @@ - -import TelegramBot from 'node-telegram-bot-api'; - -export interface TradeNotificationData { - symbol: string; - side: 'BUY' | 'SELL'; - quantity: string; - price: string; - orderId: string; - status: string; - leverage?: number; - marginType?: string; -} - -export class TelegramService { - private bot: TelegramBot; - - constructor(token: string) { - this.bot = new TelegramBot(token, { polling: false }); - } - - async sendMessage(chatId: string, message: string): Promise { - try { - console.log('Sending message to Telegram...'); - await this.bot.sendMessage(chatId, message, { parse_mode: 'HTML' }); - } catch (error) { - console.error('Error sending message to Telegram:', error); - throw error; - } - } - - /** - * Format trade execution message with rich formatting - */ - formatTradeMessage(data: TradeNotificationData): string { - const { symbol, side, quantity, price, orderId, status, leverage, marginType } = data; - - // Determine emoji based on side - const sideEmoji = side === 'BUY' ? '📈' : '📉'; - const sideText = side === 'BUY' ? 'LONG' : 'SHORT'; - - // Build message with rich formatting - let message = `✅ Trade Executed\n\n`; - message += `${sideEmoji} ${sideText} ${symbol}\n`; - message += `💰 Quantity: ${quantity}\n`; - message += `💵 Price: ${price}\n`; - message += `🆔 Order ID: ${orderId}\n`; - message += `📊 Status: ${status}\n`; - - if (leverage) { - message += `⚡ Leverage: ${leverage}x\n`; - } - - if (marginType) { - const marginTypeText = marginType === 'ISOLATED' ? '🔒 Isolated' : '🔄 Cross'; - message += `${marginTypeText}\n`; - } - - return message; - } - - /** - * Format stop order notification message - */ - formatStopOrderMessage(type: 'take_profit' | 'stop_loss', symbol: string, price: string, orderId: string): string { - const emoji = type === 'take_profit' ? '🎯' : '🛡️'; - const label = type === 'take_profit' ? 'Take Profit' : 'Stop Loss'; - - let message = `${emoji} ${label} Order Set\n\n`; - message += `📊 Symbol: ${symbol}\n`; - message += `💵 Price: ${price}\n`; - message += `🆔 Order ID: ${orderId}\n`; - - return message; - } -} diff --git a/src/services/trading-executor.ts b/src/services/trading-executor.ts index 61e429f..478cee7 100644 --- a/src/services/trading-executor.ts +++ b/src/services/trading-executor.ts @@ -1,7 +1,8 @@ import { TradingPlan } from "../types/trading"; -import { TelegramService } from "./telegram-service"; import { BinanceService, StopLossOrder, TakeProfitOrder, OrderResponse } from "./binance-service"; import { ConfigManager } from "./config-manager"; +import { TradeNotificationData, StopOrderData } from "tracker-notification"; +import SendNotification from "../utils/notification-service"; export interface ExecutionResult { success: boolean; @@ -19,8 +20,6 @@ export interface StopOrderExecutionResult extends ExecutionResult { export class TradingExecutor { private binanceService: BinanceService; private testnet: boolean; - private telegramService?: TelegramService; - private configManager: ConfigManager; constructor(apiKey?: string, apiSecret?: string, testnet?: boolean, configManager?: ConfigManager) { @@ -34,10 +33,6 @@ export class TradingExecutor { apiSecret || process.env.BINANCE_API_SECRET || "", testnet ); - this.configManager = configManager || new ConfigManager(); - if (!configManager) { - this.configManager.loadFromEnvironment(); - } } /** @@ -233,23 +228,20 @@ export class TradingExecutor { console.log(` Status: ${orderResponse.status}`); console.log(` Price: ${orderResponse.avgPrice || 'Market'}`); console.log(` Quantity: ${orderResponse.executedQty}`); - - const telegramConfig = this.configManager.getConfig().telegram; - if (telegramConfig.enabled) { - const telegramService = new TelegramService(telegramConfig.token); - const formattedMessage = telegramService.formatTradeMessage({ - symbol: orderResponse.symbol, - side: tradingPlan.side, - quantity: orderResponse.executedQty, - price: orderResponse.avgPrice || 'Market', - orderId: orderResponse.orderId.toString(), - status: orderResponse.status, - leverage: tradingPlan.leverage, - marginType: tradingPlan.marginType - }); - await telegramService.sendMessage(telegramConfig.chatId, formattedMessage); - } + const tradeNotificationData: TradeNotificationData = { + symbol: orderResponse.symbol, + side: tradingPlan.side, + quantity: orderResponse.executedQty, + price: orderResponse.avgPrice || 'Market', + orderId: orderResponse.orderId.toString(), + status: orderResponse.status, + leverage: tradingPlan.leverage, + marginType: tradingPlan.marginType + }; + + await SendNotification('trade', tradeNotificationData); + return { success: true, orderId: orderResponse.orderId.toString() @@ -308,17 +300,13 @@ export class TradingExecutor { closePosition: "true" }); takeProfitOrderId = tpOrderResponse.orderId.toString(); - const telegramConfig = this.configManager.getConfig().telegram; - if (telegramConfig.enabled) { - const telegramService = new TelegramService(telegramConfig.token); - const tpMessage = telegramService.formatStopOrderMessage( - 'take_profit', - stopOrders.takeProfitOrder!.symbol, - stopOrders.takeProfitOrder!.stopPrice.toString(), - takeProfitOrderId - ); - await telegramService.sendMessage(telegramConfig.chatId, tpMessage); - } + const stopOrderData: StopOrderData = { + type: 'take_profit', + symbol: stopOrders.takeProfitOrder!.symbol, + price: stopOrders.takeProfitOrder!.stopPrice.toString(), + orderId: takeProfitOrderId + }; + await SendNotification('stop_order', stopOrderData); console.log(`✅ Take Profit order placed: ${takeProfitOrderId}`); } catch (tpError) { console.error(`❌ Failed to place Take Profit order: ${tpError instanceof Error ? tpError.message : 'Unknown error'}`); @@ -341,18 +329,13 @@ export class TradingExecutor { closePosition: "true" }); stopLossOrderId = slOrderResponse.orderId.toString(); - // Send Telegram notification for stop loss - const telegramConfig = this.configManager.getConfig().telegram; - if (telegramConfig.enabled) { - const telegramService = new TelegramService(telegramConfig.token); - const slMessage = telegramService.formatStopOrderMessage( - 'stop_loss', - stopOrders.stopLossOrder!.symbol, - stopOrders.stopLossOrder!.stopPrice.toString(), - stopLossOrderId - ); - await telegramService.sendMessage(telegramConfig.chatId, slMessage); - } + const stopOrderData: StopOrderData = { + type: 'stop_loss', + symbol: stopOrders.stopLossOrder!.symbol, + price: stopOrders.stopLossOrder!.stopPrice.toString(), + orderId: stopLossOrderId + }; + await SendNotification('stop_order', stopOrderData); console.log(`✅ Stop Loss order placed: ${stopLossOrderId}`); } catch (slError) { console.error(`❌ Failed to place Stop Loss order: ${slError instanceof Error ? slError.message : 'Unknown error'}`); diff --git a/src/utils/notification-service.ts b/src/utils/notification-service.ts new file mode 100644 index 0000000..962a77b --- /dev/null +++ b/src/utils/notification-service.ts @@ -0,0 +1,41 @@ +import { NotificationManager, TelegramNotificationProvider, SlackNotificationProvider, TradeNotificationData, StopOrderData } from 'tracker-notification'; +require('dotenv').config(); + +export default async function SendNotification(type: 'trade' | 'stop_order', tradeData: TradeNotificationData | StopOrderData) { + try { + // 1. Create notification manager + const notifications = new NotificationManager(); + + if (process.env.NOTIFICATION_TELEGRAM_ENABLED === 'true') { + // 2. Add telegram provider directly + const telegramProvider = new TelegramNotificationProvider({ + botToken: process.env.TELEGRAM_BOT_TOKEN || '123456:ABC-DEF', + chatId: process.env.TELEGRAM_CHAT_ID || '123456789', + parseMode: 'Markdown' + }); + notifications.addProvider(telegramProvider); + } + + if (process.env.NOTIFICATION_SLACK_ENABLED === 'true') { + const slackProvider = new SlackNotificationProvider({ + botToken: process.env.SLACK_BOT_TOKEN || 'xoxb-test-token', + channelId: process.env.SLACK_CHANNEL_ID || 'C123456789', + username: 'Trading Bot' + }); + notifications.addProvider(slackProvider); + } + + if (type === 'trade') { + await notifications.notifyTrade(tradeData as TradeNotificationData); + console.log('\n🎉 Trade notification sent successfully!'); + } else if (type === 'stop_order') { + await notifications.notifyStopOrder(tradeData as StopOrderData); + console.log('\n🎉 Stop order notification sent successfully!'); + } + + + } catch (error) { + console.error('❌ Notification service failed:', error); + } +} +