From 82beac0920c819ad3ccafb728a17b12b8d3efb27 Mon Sep 17 00:00:00 2001 From: icy Date: Thu, 30 Oct 2025 20:51:22 +0800 Subject: [PATCH 01/67] sync fork --- Dockerfile | 2 +- README.md | 262 ++++--- api/server.go | 302 +++++++- config/config.go | 138 ---- config/database.go | 390 ++++++++++ docker-compose.yml | 2 - go.mod | 1 + go.sum | 2 + main.go | 97 ++- manager/trader_manager.go | 208 +++++- web/.dockerignore | 1 - web/Dockerfile | 2 +- web/src/App.tsx | 166 ++-- web/src/components/AITradersPage.tsx | 998 +++++++++++++++++++++++++ web/src/components/ComparisonChart.tsx | 336 --------- web/src/components/CompetitionPage.tsx | 251 ------- web/src/i18n/translations.ts | 84 ++- web/src/lib/api.ts | 77 +- web/src/types.ts | 58 +- web/vite.config.ts | 2 +- 20 files changed, 2337 insertions(+), 1042 deletions(-) delete mode 100644 config/config.go create mode 100644 config/database.go create mode 100644 web/src/components/AITradersPage.tsx delete mode 100644 web/src/components/ComparisonChart.tsx delete mode 100644 web/src/components/CompetitionPage.tsx diff --git a/Dockerfile b/Dockerfile index 49f54a09..25c389c1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # 构建阶段 -FROM golang:1.21-alpine AS builder +FROM golang:1.25-alpine AS builder # 安装必要的构建工具 RUN apk add --no-cache git gcc musl-dev diff --git a/README.md b/README.md index c957998a..2cf5671b 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,16 @@ -# 🤖 NOFX - AI-Driven Crypto Futures Auto Trading Competition System +# 🤖 NOFX - Multi-AI Model Automated Trading Platform [![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go)](https://golang.org/) [![React](https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react)](https://reactjs.org/) [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript)](https://www.typescriptlang.org/) +[![SQLite](https://img.shields.io/badge/SQLite-3+-003B57?style=flat&logo=sqlite)](https://sqlite.org/) [![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) **Languages:** [English](README.md) | [中文](README.zh-CN.md) | [Українська](README.uk.md) | [Русский](README.ru.md) --- -An automated crypto futures trading system powered by **DeepSeek/Qwen AI**, supporting **Binance and Hyperliquid exchanges**, **multi-AI model live trading competition**, featuring comprehensive market analysis, AI decision-making, **self-learning mechanism**, and professional Web monitoring interface. +A modern automated crypto futures trading platform powered by **DeepSeek/Qwen AI**, supporting **Binance and Hyperliquid exchanges**. Create and manage multiple AI traders with dynamic configuration through a web interface. Features comprehensive market analysis, AI decision-making, and professional monitoring dashboard. > ⚠️ **Risk Warning**: This system is experimental. AI auto-trading carries significant risks. Strongly recommended for learning/research purposes or testing with small amounts only! @@ -23,40 +24,41 @@ Join our Telegram developer community to discuss, share ideas, and get support: ## 🆕 What's New (Latest Update) -### 🚀 Hyperliquid Exchange Support Added! +### 🚀 Complete System Transformation - Web-Based Configuration! -NOFX now supports **Hyperliquid** - a high-performance decentralized perpetual futures exchange! +NOFX has been **completely transformed** from a static config-based system to a **dynamic web-based trading platform**! -**Key Features:** -- ✅ Full trading support (long/short, leverage, stop-loss/take-profit) -- ✅ Automatic precision handling (order size & price) -- ✅ Unified trader interface (seamless exchange switching) -- ✅ Support for both mainnet and testnet -- ✅ No API keys needed - just your Ethereum private key +**Major Changes:** +- ✅ **Web-Based Configuration**: Create and manage AI traders through a modern web interface +- ✅ **Database-Driven Architecture**: SQLite database replaces static JSON configuration +- ✅ **Separate AI Models & Exchanges**: Configure AI models and exchanges independently +- ✅ **Dynamic Trader Creation**: Create traders by combining configured AI models and exchanges +- ✅ **Real-Time Management**: Start/stop traders, update configurations without restart +- ✅ **No Default Traders**: Clean slate - create only the traders you need -**Why Hyperliquid?** -- 🔥 Lower fees than centralized exchanges -- 🔒 Non-custodial - you control your funds -- ⚡ Fast execution with on-chain settlement -- 🌍 No KYC required +**New Workflow:** +1. **Configure AI Models**: Add your DeepSeek/Qwen API keys through the web interface +2. **Configure Exchanges**: Set up Binance/Hyperliquid API credentials +3. **Create Traders**: Combine any AI model with any exchange to create custom traders +4. **Monitor & Control**: Start/stop traders and monitor performance in real-time -**Quick Start:** -1. Get your MetaMask private key (remove `0x` prefix) -2. Set `"exchange": "hyperliquid"` in config.json -3. Add `"hyperliquid_private_key": "your_key"` -4. Start trading! +**Why This Update?** +- 🎯 **User-Friendly**: No more editing JSON files or server restarts +- 🔧 **Flexible**: Mix and match different AI models with different exchanges +- 📊 **Scalable**: Create unlimited trader combinations +- 🔒 **Secure**: Database storage with proper data management -See [Configuration Guide](#-alternative-using-hyperliquid-exchange) for details. +See [Quick Start](#-quick-start) for the new setup process! --- ## ✨ Core Features -### 🏆 Multi-AI Competition Mode -- **Qwen vs DeepSeek** live trading battle -- Independent account management and decision logs -- Real-time performance comparison charts -- ROI PK and win rate statistics +### 🎛️ Web-Based Configuration Management +- **Dynamic AI Model Setup**: Configure DeepSeek and Qwen API keys through web interface +- **Exchange Management**: Set up Binance and Hyperliquid credentials independently +- **Flexible Trader Creation**: Mix any AI model with any exchange +- **Real-Time Control**: Start/stop traders without system restart ### 🧠 AI Self-Learning Mechanism (NEW!) - **Historical Feedback**: Analyzes last 20 cycles of trading performance before each decision @@ -195,22 +197,13 @@ Before using this system, you need a Binance Futures account. **Use our referral ## 🚀 Quick Start -### 🐳 Option A: Docker One-Click Deployment (EASIEST - Recommended for Beginners!) +### 🐳 Option A: Docker One-Click Deployment (EASIEST - Recommended!) -**⚡ Start trading in 3 simple steps with Docker - No installation needed!** +**⚡ Start the platform in 2 simple steps with Docker - No installation needed!** -Docker automatically handles all dependencies (Go, Node.js, TA-Lib) and environment setup. Perfect for beginners! +Docker automatically handles all dependencies (Go, Node.js, TA-Lib, SQLite) and environment setup. -#### Step 1: Prepare Configuration -```bash -# Copy configuration template -cp config.json.example config.json - -# Edit and fill in your API keys -nano config.json # or use any editor -``` - -#### Step 2: One-Click Start +#### Step 1: One-Click Start ```bash # Option 1: Use convenience script (Recommended) chmod +x start.sh @@ -220,10 +213,16 @@ chmod +x start.sh docker-compose up -d --build ``` -#### Step 3: Access Dashboard +#### Step 2: Access Web Interface Open your browser and visit: **http://localhost:3000** -**That's it! 🎉** Your AI trading system is now running! +**That's it! 🎉** Your AI trading platform is now running! + +#### Initial Setup (Through Web Interface) +1. **Configure AI Models**: Add your DeepSeek/Qwen API keys +2. **Configure Exchanges**: Set up Binance/Hyperliquid credentials +3. **Create Traders**: Combine AI models with exchanges +4. **Start Trading**: Launch your configured traders #### Manage Your System ```bash @@ -328,67 +327,73 @@ Before configuring the system, you need to obtain AI API keys. Choose one of the --- -### 5. System Configuration +### 5. Start the System -**Two configuration modes available:** -- **🌟 Beginner Mode**: Single trader + default coins (recommended!) -- **⚔️ Expert Mode**: Multiple traders competition - -#### 🌟 Beginner Mode Configuration (Recommended) - -**Step 1**: Copy and rename the example config file +#### **Step 1: Start the Backend** ```bash -cp config.json.example config.json +# Build the program (first time only, or after code changes) +go build -o nofx + +# Start the backend +./nofx ``` -**Step 2**: Edit `config.json` with your API keys +**What you should see:** -```json -{ - "traders": [ - { - "id": "my_trader", - "name": "My AI Trader", - "ai_model": "deepseek", - "binance_api_key": "YOUR_BINANCE_API_KEY", - "binance_secret_key": "YOUR_BINANCE_SECRET_KEY", - "use_qwen": false, - "deepseek_key": "sk-xxxxxxxxxxxxx", - "qwen_key": "", - "initial_balance": 1000.0, - "scan_interval_minutes": 3 - } - ], - "use_default_coins": true, - "coin_pool_api_url": "", - "oi_top_api_url": "", - "api_server_port": 8080 -} +``` +╔════════════════════════════════════════════════════════════╗ +║ 🤖 AI多模型交易系统 - 支持 DeepSeek & Qwen ║ +╚════════════════════════════════════════════════════════════╝ + +🤖 数据库中的AI交易员配置: + • 暂无配置的交易员,请通过Web界面创建 + +🌐 API服务器启动在 http://localhost:8081 ``` -**Step 3**: Replace placeholders with your actual keys +#### **Step 2: Start the Frontend** -| Placeholder | Replace With | Where to Get | -|------------|--------------|--------------| -| `YOUR_BINANCE_API_KEY` | Your Binance API Key | Binance → Account → API Management | -| `YOUR_BINANCE_SECRET_KEY` | Your Binance Secret Key | Same as above | -| `sk-xxxxxxxxxxxxx` | Your DeepSeek API Key | [platform.deepseek.com](https://platform.deepseek.com) | +Open a **NEW terminal window**, then: -**Step 4**: Adjust initial balance (optional) +```bash +cd web +npm run dev +``` -- `initial_balance`: Set to your actual Binance futures account balance -- Used to calculate profit/loss percentage -- Example: If you have 500 USDT, set `"initial_balance": 500.0` +#### **Step 3: Access the Web Interface** -**✅ Configuration Checklist:** +Open your browser and visit: **🌐 http://localhost:3000** -- [ ] Binance API key filled in (no quotes issues) -- [ ] Binance Secret key filled in (no quotes issues) -- [ ] DeepSeek API key filled in (starts with `sk-`) -- [ ] `use_default_coins` set to `true` (for beginners) -- [ ] `initial_balance` matches your account balance -- [ ] File saved as `config.json` (not `.example`) +### 6. Configure Through Web Interface + +**Now configure everything through the web interface - no more JSON editing!** + +#### **Step 1: Configure AI Models** +1. Click "AI模型配置" button +2. Enable DeepSeek or Qwen (or both) +3. Enter your API keys +4. Save configuration + +#### **Step 2: Configure Exchanges** +1. Click "交易所配置" button +2. Enable Binance or Hyperliquid (or both) +3. Enter your API credentials +4. Save configuration + +#### **Step 3: Create Traders** +1. Click "创建交易员" button +2. Select an AI model (must be configured first) +3. Select an exchange (must be configured first) +4. Set initial balance and trader name +5. Create trader + +#### **Step 4: Start Trading** +- Your traders will appear in the main interface +- Use Start/Stop buttons to control them +- Monitor performance in real-time + +**✅ No more JSON file editing - everything is done through the web interface!** --- @@ -866,14 +871,26 @@ Each decision cycle (default 3 minutes), the system executes the following intel ## 🎛️ API Endpoints -### Competition Related +### Configuration Management ```bash -GET /api/competition # Competition leaderboard (all traders) -GET /api/traders # Trader list +GET /api/models # Get AI model configurations +PUT /api/models # Update AI model configurations +GET /api/exchanges # Get exchange configurations +PUT /api/exchanges # Update exchange configurations ``` -### Single Trader Related +### Trader Management + +```bash +GET /api/traders # List all traders +POST /api/traders # Create new trader +DELETE /api/traders/:id # Delete trader +POST /api/traders/:id/start # Start trader +POST /api/traders/:id/stop # Stop trader +``` + +### Trading Data & Monitoring ```bash GET /api/status?trader_id=xxx # System status @@ -882,13 +899,13 @@ GET /api/positions?trader_id=xxx # Position list GET /api/equity-history?trader_id=xxx # Equity history (chart data) GET /api/decisions/latest?trader_id=xxx # Latest 5 decisions GET /api/statistics?trader_id=xxx # Statistics +GET /api/performance?trader_id=xxx # AI performance analysis ``` ### System Endpoints ```bash GET /health # Health check -GET /api/config # System configuration ``` --- @@ -980,6 +997,61 @@ sudo apt-get install libta-lib0-dev ## 🔄 Changelog +### v3.0.0 (2025-10-30) - Major Architecture Transformation + +**🚀 Complete System Redesign - Web-Based Configuration Platform** + +This is a **major breaking update** that completely transforms NOFX from a static config-based system to a modern web-based trading platform. + +**Revolutionary Changes:** + +**1. Database-Driven Architecture** +- ✅ **SQLite Integration**: Replaced static JSON config with SQLite database +- ✅ **Persistent Storage**: All configurations stored in database with automatic timestamps +- ✅ **Data Integrity**: Foreign key relationships and triggers for data consistency +- ✅ **Schema Design**: Separate tables for AI models, exchanges, traders, and system config + +**2. Web-Based Configuration Interface** +- ✅ **No More JSON Editing**: Complete web-based configuration management +- ✅ **AI Model Setup**: Configure DeepSeek/Qwen API keys through web interface +- ✅ **Exchange Management**: Set up Binance/Hyperliquid credentials independently +- ✅ **Dynamic Trader Creation**: Create traders by combining any AI model with any exchange +- ✅ **Real-Time Control**: Start/stop traders without system restart + +**3. Flexible Architecture** +- ✅ **Separation of Concerns**: AI models and exchanges configured independently +- ✅ **Mix & Match**: Create unlimited combinations (e.g., Qwen + Binance, DeepSeek + Hyperliquid) +- ✅ **Scalable Design**: Support for unlimited traders and configurations +- ✅ **Clean Slate**: No default traders - create only what you need + +**4. Enhanced API Layer** +- ✅ **RESTful Design**: Complete CRUD operations for all configuration entities +- ✅ **New Endpoints**: + - `GET/PUT /api/models` - AI model configuration + - `GET/PUT /api/exchanges` - Exchange configuration + - `POST/DELETE /api/traders` - Trader management + - `POST /api/traders/:id/start|stop` - Trader control +- ✅ **Updated Documentation**: All API endpoints documented + +**5. Modernized Codebase** +- ✅ **Type Safety**: Proper separation of legacy and new configuration types +- ✅ **Database Abstraction**: Clean database layer with prepared statements +- ✅ **Error Handling**: Comprehensive error handling and validation +- ✅ **Code Organization**: Better separation between database, API, and business logic + +**Migration Notes:** +- ⚠️ **Breaking Change**: Old `config.json` files are no longer used +- ⚠️ **Fresh Start**: All configurations must be redone through web interface +- ✅ **Easier Setup**: Web-based configuration is much more user-friendly +- ✅ **Better UX**: No more server restarts for configuration changes + +**Why This Update Matters:** +- 🎯 **User Experience**: Much easier to configure and manage +- 🔧 **Flexibility**: Create any combination of AI models and exchanges +- 📊 **Scalability**: Support for complex multi-trader setups +- 🔒 **Reliability**: Database ensures data persistence and consistency +- 🚀 **Future-Proof**: Foundation for advanced features like trader templates, backtesting, etc. + ### v2.0.2 (2025-10-29) **Critical Bug Fixes - Trade History & Performance Analysis:** @@ -1091,7 +1163,7 @@ Issues and Pull Requests are welcome! --- -**Last Updated**: 2025-10-29 (v2.0.2) +**Last Updated**: 2025-10-30 (v3.0.0) **⚡ Explore the possibilities of quantitative trading with the power of AI!** diff --git a/api/server.go b/api/server.go index 32d24c51..4d761585 100644 --- a/api/server.go +++ b/api/server.go @@ -4,7 +4,9 @@ import ( "fmt" "log" "net/http" + "nofx/config" "nofx/manager" + "time" "github.com/gin-gonic/gin" ) @@ -13,11 +15,12 @@ import ( type Server struct { router *gin.Engine traderManager *manager.TraderManager + database *config.Database port int } // NewServer 创建API服务器 -func NewServer(traderManager *manager.TraderManager, port int) *Server { +func NewServer(traderManager *manager.TraderManager, database *config.Database, port int) *Server { // 设置为Release模式(减少日志输出) gin.SetMode(gin.ReleaseMode) @@ -29,6 +32,7 @@ func NewServer(traderManager *manager.TraderManager, port int) *Server { s := &Server{ router: router, traderManager: traderManager, + database: database, port: port, } @@ -62,11 +66,20 @@ func (s *Server) setupRoutes() { // API路由组 api := s.router.Group("/api") { - // 竞赛总览 - api.GET("/competition", s.handleCompetition) - - // Trader列表 + // AI交易员管理 api.GET("/traders", s.handleTraderList) + api.POST("/traders", s.handleCreateTrader) + api.DELETE("/traders/:id", s.handleDeleteTrader) + api.POST("/traders/:id/start", s.handleStartTrader) + api.POST("/traders/:id/stop", s.handleStopTrader) + + // AI模型配置 + api.GET("/models", s.handleGetModelConfigs) + api.PUT("/models", s.handleUpdateModelConfigs) + + // 交易所配置 + api.GET("/exchanges", s.handleGetExchangeConfigs) + api.PUT("/exchanges", s.handleUpdateExchangeConfigs) // 指定trader的数据(使用query参数 ?trader_id=xxx) api.GET("/status", s.handleStatus) @@ -102,28 +115,266 @@ func (s *Server) getTraderFromQuery(c *gin.Context) (*manager.TraderManager, str return s.traderManager, traderID, nil } -// handleCompetition 竞赛总览(对比所有trader) -func (s *Server) handleCompetition(c *gin.Context) { - comparison, err := s.traderManager.GetComparisonData() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("获取对比数据失败: %v", err), - }) +// AI交易员管理相关结构体 +type CreateTraderRequest struct { + Name string `json:"name" binding:"required"` + AIModelID string `json:"ai_model_id" binding:"required"` + ExchangeID string `json:"exchange_id" binding:"required"` + InitialBalance float64 `json:"initial_balance"` +} + +type ModelConfig struct { + ID string `json:"id"` + Name string `json:"name"` + Provider string `json:"provider"` + Enabled bool `json:"enabled"` + APIKey string `json:"apiKey,omitempty"` +} + +type ExchangeConfig struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` // "cex" or "dex" + Enabled bool `json:"enabled"` + APIKey string `json:"apiKey,omitempty"` + SecretKey string `json:"secretKey,omitempty"` + Testnet bool `json:"testnet,omitempty"` +} + +type UpdateModelConfigRequest struct { + Models map[string]struct { + Enabled bool `json:"enabled"` + APIKey string `json:"api_key"` + } `json:"models"` +} + +type UpdateExchangeConfigRequest struct { + Exchanges map[string]struct { + Enabled bool `json:"enabled"` + APIKey string `json:"api_key"` + SecretKey string `json:"secret_key"` + Testnet bool `json:"testnet"` + } `json:"exchanges"` +} + +// handleCreateTrader 创建新的AI交易员 +func (s *Server) handleCreateTrader(c *gin.Context) { + var req CreateTraderRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - c.JSON(http.StatusOK, comparison) + + // 生成交易员ID + traderID := fmt.Sprintf("%s_%s_%d", req.ExchangeID, req.AIModelID, time.Now().Unix()) + + // 创建交易员配置 + trader := &config.TraderConfig{ + ID: traderID, + Name: req.Name, + AIModelID: req.AIModelID, + ExchangeID: req.ExchangeID, + InitialBalance: req.InitialBalance, + ScanIntervalMinutes: 3, // 默认3分钟 + IsRunning: false, + } + + // 保存到数据库 + err := s.database.CreateTrader(trader) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("创建交易员失败: %v", err)}) + return + } + + log.Printf("✓ 创建交易员成功: %s (模型: %s, 交易所: %s)", req.Name, req.AIModelID, req.ExchangeID) + + c.JSON(http.StatusCreated, gin.H{ + "trader_id": traderID, + "trader_name": req.Name, + "ai_model": req.AIModelID, + "is_running": false, + }) +} + +// handleDeleteTrader 删除交易员 +func (s *Server) handleDeleteTrader(c *gin.Context) { + traderID := c.Param("id") + + // 从数据库删除 + err := s.database.DeleteTrader(traderID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("删除交易员失败: %v", err)}) + return + } + + // 如果交易员正在运行,先停止它 + if trader, err := s.traderManager.GetTrader(traderID); err == nil { + status := trader.GetStatus() + if isRunning, ok := status["is_running"].(bool); ok && isRunning { + trader.Stop() + log.Printf("⏹ 已停止运行中的交易员: %s", traderID) + } + } + + log.Printf("✓ 交易员已删除: %s", traderID) + c.JSON(http.StatusOK, gin.H{"message": "交易员已删除"}) +} + +// handleStartTrader 启动交易员 +func (s *Server) handleStartTrader(c *gin.Context) { + traderID := c.Param("id") + + trader, err := s.traderManager.GetTrader(traderID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) + return + } + + // 检查交易员是否已经在运行 + status := trader.GetStatus() + if isRunning, ok := status["is_running"].(bool); ok && isRunning { + c.JSON(http.StatusBadRequest, gin.H{"error": "交易员已在运行中"}) + return + } + + // 启动交易员 + go func() { + log.Printf("▶️ 启动交易员 %s (%s)", traderID, trader.GetName()) + if err := trader.Run(); err != nil { + log.Printf("❌ 交易员 %s 运行错误: %v", trader.GetName(), err) + } + }() + + // 更新数据库中的运行状态 + err = s.database.UpdateTraderStatus(traderID, true) + if err != nil { + log.Printf("⚠️ 更新交易员状态失败: %v", err) + } + + log.Printf("✓ 交易员 %s 已启动", trader.GetName()) + c.JSON(http.StatusOK, gin.H{"message": "交易员已启动"}) +} + +// handleStopTrader 停止交易员 +func (s *Server) handleStopTrader(c *gin.Context) { + traderID := c.Param("id") + + trader, err := s.traderManager.GetTrader(traderID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) + return + } + + // 检查交易员是否正在运行 + status := trader.GetStatus() + if isRunning, ok := status["is_running"].(bool); ok && !isRunning { + c.JSON(http.StatusBadRequest, gin.H{"error": "交易员已停止"}) + return + } + + // 停止交易员 + trader.Stop() + + // 更新数据库中的运行状态 + err = s.database.UpdateTraderStatus(traderID, false) + if err != nil { + log.Printf("⚠️ 更新交易员状态失败: %v", err) + } + + log.Printf("⏹ 交易员 %s 已停止", trader.GetName()) + c.JSON(http.StatusOK, gin.H{"message": "交易员已停止"}) +} + +// handleGetModelConfigs 获取AI模型配置 +func (s *Server) handleGetModelConfigs(c *gin.Context) { + models, err := s.database.GetAIModels() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("获取AI模型配置失败: %v", err)}) + return + } + + c.JSON(http.StatusOK, models) +} + +// handleUpdateModelConfigs 更新AI模型配置 +func (s *Server) handleUpdateModelConfigs(c *gin.Context) { + var req UpdateModelConfigRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 更新每个模型的配置 + for modelID, modelData := range req.Models { + err := s.database.UpdateAIModel(modelID, modelData.Enabled, modelData.APIKey) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新模型 %s 失败: %v", modelID, err)}) + return + } + } + + log.Printf("✓ AI模型配置已更新: %+v", req.Models) + c.JSON(http.StatusOK, gin.H{"message": "模型配置已更新"}) +} + +// handleGetExchangeConfigs 获取交易所配置 +func (s *Server) handleGetExchangeConfigs(c *gin.Context) { + exchanges, err := s.database.GetExchanges() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("获取交易所配置失败: %v", err)}) + return + } + + c.JSON(http.StatusOK, exchanges) +} + +// handleUpdateExchangeConfigs 更新交易所配置 +func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) { + var req UpdateExchangeConfigRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 更新每个交易所的配置 + for exchangeID, exchangeData := range req.Exchanges { + err := s.database.UpdateExchange(exchangeID, exchangeData.Enabled, exchangeData.APIKey, exchangeData.SecretKey, exchangeData.Testnet) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新交易所 %s 失败: %v", exchangeID, err)}) + return + } + } + + log.Printf("✓ 交易所配置已更新: %+v", req.Exchanges) + c.JSON(http.StatusOK, gin.H{"message": "交易所配置已更新"}) } // handleTraderList trader列表 func (s *Server) handleTraderList(c *gin.Context) { - traders := s.traderManager.GetAllTraders() - result := make([]map[string]interface{}, 0, len(traders)) + traders, err := s.database.GetTraders() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("获取交易员列表失败: %v", err)}) + return + } + + result := make([]map[string]interface{}, 0, len(traders)) + for _, trader := range traders { + // 获取实时运行状态 + isRunning := trader.IsRunning + if at, err := s.traderManager.GetTrader(trader.ID); err == nil { + status := at.GetStatus() + if running, ok := status["is_running"].(bool); ok { + isRunning = running + } + } - for _, t := range traders { result = append(result, map[string]interface{}{ - "trader_id": t.GetID(), - "trader_name": t.GetName(), - "ai_model": t.GetAIModel(), + "trader_id": trader.ID, + "trader_name": trader.Name, + "ai_model": trader.AIModelID, + "exchange_id": trader.ExchangeID, + "is_running": isRunning, + "initial_balance": trader.InitialBalance, }) } @@ -405,8 +656,16 @@ func (s *Server) Start() error { addr := fmt.Sprintf(":%d", s.port) log.Printf("🌐 API服务器启动在 http://localhost%s", addr) log.Printf("📊 API文档:") - log.Printf(" • GET /api/competition - 竞赛总览(对比所有trader)") - log.Printf(" • GET /api/traders - Trader列表") + log.Printf(" • GET /health - 健康检查") + log.Printf(" • GET /api/traders - AI交易员列表") + log.Printf(" • POST /api/traders - 创建新的AI交易员") + log.Printf(" • DELETE /api/traders/:id - 删除AI交易员") + log.Printf(" • POST /api/traders/:id/start - 启动AI交易员") + log.Printf(" • POST /api/traders/:id/stop - 停止AI交易员") + log.Printf(" • GET /api/models - 获取AI模型配置") + log.Printf(" • PUT /api/models - 更新AI模型配置") + log.Printf(" • GET /api/exchanges - 获取交易所配置") + log.Printf(" • PUT /api/exchanges - 更新交易所配置") log.Printf(" • GET /api/status?trader_id=xxx - 指定trader的系统状态") log.Printf(" • GET /api/account?trader_id=xxx - 指定trader的账户信息") log.Printf(" • GET /api/positions?trader_id=xxx - 指定trader的持仓列表") @@ -415,7 +674,6 @@ func (s *Server) Start() error { log.Printf(" • GET /api/statistics?trader_id=xxx - 指定trader的统计信息") log.Printf(" • GET /api/equity-history?trader_id=xxx - 指定trader的收益率历史数据") log.Printf(" • GET /api/performance?trader_id=xxx - 指定trader的AI学习表现分析") - log.Printf(" • GET /health - 健康检查") log.Println() return s.router.Run(addr) diff --git a/config/config.go b/config/config.go deleted file mode 100644 index 67b5e559..00000000 --- a/config/config.go +++ /dev/null @@ -1,138 +0,0 @@ -package config - -import ( - "encoding/json" - "fmt" - "os" - "time" -) - -// TraderConfig 单个trader的配置 -type TraderConfig struct { - ID string `json:"id"` - Name string `json:"name"` - AIModel string `json:"ai_model"` // "qwen" or "deepseek" - - // 交易平台选择(二选一) - Exchange string `json:"exchange"` // "binance" or "hyperliquid" - - // 币安配置 - BinanceAPIKey string `json:"binance_api_key,omitempty"` - BinanceSecretKey string `json:"binance_secret_key,omitempty"` - - // Hyperliquid配置 - HyperliquidPrivateKey string `json:"hyperliquid_private_key,omitempty"` - HyperliquidTestnet bool `json:"hyperliquid_testnet,omitempty"` - - // AI配置 - QwenKey string `json:"qwen_key,omitempty"` - DeepSeekKey string `json:"deepseek_key,omitempty"` - - InitialBalance float64 `json:"initial_balance"` - ScanIntervalMinutes int `json:"scan_interval_minutes"` -} - -// Config 总配置 -type Config struct { - Traders []TraderConfig `json:"traders"` - UseDefaultCoins bool `json:"use_default_coins"` // 是否使用默认主流币种列表 - CoinPoolAPIURL string `json:"coin_pool_api_url"` - OITopAPIURL string `json:"oi_top_api_url"` - APIServerPort int `json:"api_server_port"` - MaxDailyLoss float64 `json:"max_daily_loss"` - MaxDrawdown float64 `json:"max_drawdown"` - StopTradingMinutes int `json:"stop_trading_minutes"` -} - -// LoadConfig 从文件加载配置 -func LoadConfig(filename string) (*Config, error) { - data, err := os.ReadFile(filename) - if err != nil { - return nil, fmt.Errorf("读取配置文件失败: %w", err) - } - - var config Config - if err := json.Unmarshal(data, &config); err != nil { - return nil, fmt.Errorf("解析配置文件失败: %w", err) - } - - // 设置默认值:如果use_default_coins未设置(为false)且没有配置coin_pool_api_url,则默认使用默认币种列表 - if !config.UseDefaultCoins && config.CoinPoolAPIURL == "" { - config.UseDefaultCoins = true - } - - // 验证配置 - if err := config.Validate(); err != nil { - return nil, fmt.Errorf("配置验证失败: %w", err) - } - - return &config, nil -} - -// Validate 验证配置有效性 -func (c *Config) Validate() error { - if len(c.Traders) == 0 { - return fmt.Errorf("至少需要配置一个trader") - } - - traderIDs := make(map[string]bool) - for i, trader := range c.Traders { - if trader.ID == "" { - return fmt.Errorf("trader[%d]: ID不能为空", i) - } - if traderIDs[trader.ID] { - return fmt.Errorf("trader[%d]: ID '%s' 重复", i, trader.ID) - } - traderIDs[trader.ID] = true - - if trader.Name == "" { - return fmt.Errorf("trader[%d]: Name不能为空", i) - } - if trader.AIModel != "qwen" && trader.AIModel != "deepseek" { - return fmt.Errorf("trader[%d]: ai_model必须是 'qwen' 或 'deepseek'", i) - } - - // 验证交易平台配置 - if trader.Exchange == "" { - trader.Exchange = "binance" // 默认使用币安 - } - if trader.Exchange != "binance" && trader.Exchange != "hyperliquid" { - return fmt.Errorf("trader[%d]: exchange必须是 'binance' 或 'hyperliquid'", i) - } - - // 根据平台验证对应的密钥 - if trader.Exchange == "binance" { - if trader.BinanceAPIKey == "" || trader.BinanceSecretKey == "" { - return fmt.Errorf("trader[%d]: 使用币安时必须配置binance_api_key和binance_secret_key", i) - } - } else if trader.Exchange == "hyperliquid" { - if trader.HyperliquidPrivateKey == "" { - return fmt.Errorf("trader[%d]: 使用Hyperliquid时必须配置hyperliquid_private_key", i) - } - } - - if trader.AIModel == "qwen" && trader.QwenKey == "" { - return fmt.Errorf("trader[%d]: 使用Qwen时必须配置qwen_key", i) - } - if trader.AIModel == "deepseek" && trader.DeepSeekKey == "" { - return fmt.Errorf("trader[%d]: 使用DeepSeek时必须配置deepseek_key", i) - } - if trader.InitialBalance <= 0 { - return fmt.Errorf("trader[%d]: initial_balance必须大于0", i) - } - if trader.ScanIntervalMinutes <= 0 { - trader.ScanIntervalMinutes = 3 // 默认3分钟 - } - } - - if c.APIServerPort <= 0 { - c.APIServerPort = 8080 // 默认8080端口 - } - - return nil -} - -// GetScanInterval 获取扫描间隔 -func (tc *TraderConfig) GetScanInterval() time.Duration { - return time.Duration(tc.ScanIntervalMinutes) * time.Minute -} diff --git a/config/database.go b/config/database.go new file mode 100644 index 00000000..8e1c193a --- /dev/null +++ b/config/database.go @@ -0,0 +1,390 @@ +package config + +import ( + "database/sql" + "fmt" + "time" + + _ "github.com/mattn/go-sqlite3" +) + +// Database 配置数据库 +type Database struct { + db *sql.DB +} + +// NewDatabase 创建配置数据库 +func NewDatabase(dbPath string) (*Database, error) { + db, err := sql.Open("sqlite3", dbPath) + if err != nil { + return nil, fmt.Errorf("打开数据库失败: %w", err) + } + + database := &Database{db: db} + if err := database.createTables(); err != nil { + return nil, fmt.Errorf("创建表失败: %w", err) + } + + if err := database.initDefaultData(); err != nil { + return nil, fmt.Errorf("初始化默认数据失败: %w", err) + } + + return database, nil +} + +// createTables 创建数据库表 +func (d *Database) createTables() error { + queries := []string{ + // AI模型配置表 + `CREATE TABLE IF NOT EXISTS ai_models ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + provider TEXT NOT NULL, + enabled BOOLEAN DEFAULT 0, + api_key TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`, + + // 交易所配置表 + `CREATE TABLE IF NOT EXISTS exchanges ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + type TEXT NOT NULL, -- 'cex' or 'dex' + enabled BOOLEAN DEFAULT 0, + api_key TEXT DEFAULT '', + secret_key TEXT DEFAULT '', + testnet BOOLEAN DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`, + + // 交易员配置表 + `CREATE TABLE IF NOT EXISTS traders ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + ai_model_id TEXT NOT NULL, + exchange_id TEXT NOT NULL, + initial_balance REAL NOT NULL, + scan_interval_minutes INTEGER DEFAULT 3, + is_running BOOLEAN DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (ai_model_id) REFERENCES ai_models(id), + FOREIGN KEY (exchange_id) REFERENCES exchanges(id) + )`, + + // 系统配置表 + `CREATE TABLE IF NOT EXISTS system_config ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`, + + // 触发器:自动更新 updated_at + `CREATE TRIGGER IF NOT EXISTS update_ai_models_updated_at + AFTER UPDATE ON ai_models + BEGIN + UPDATE ai_models SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; + END`, + + `CREATE TRIGGER IF NOT EXISTS update_exchanges_updated_at + AFTER UPDATE ON exchanges + BEGIN + UPDATE exchanges SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; + END`, + + `CREATE TRIGGER IF NOT EXISTS update_traders_updated_at + AFTER UPDATE ON traders + BEGIN + UPDATE traders SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; + END`, + + `CREATE TRIGGER IF NOT EXISTS update_system_config_updated_at + AFTER UPDATE ON system_config + BEGIN + UPDATE system_config SET updated_at = CURRENT_TIMESTAMP WHERE key = NEW.key; + END`, + } + + for _, query := range queries { + if _, err := d.db.Exec(query); err != nil { + return fmt.Errorf("执行SQL失败 [%s]: %w", query, err) + } + } + + return nil +} + +// initDefaultData 初始化默认数据 +func (d *Database) initDefaultData() error { + // 初始化AI模型 + aiModels := []struct { + id, name, provider string + }{ + {"deepseek", "DeepSeek", "deepseek"}, + {"qwen", "Qwen", "qwen"}, + } + + for _, model := range aiModels { + _, err := d.db.Exec(` + INSERT OR IGNORE INTO ai_models (id, name, provider, enabled) + VALUES (?, ?, ?, 0) + `, model.id, model.name, model.provider) + if err != nil { + return fmt.Errorf("初始化AI模型失败: %w", err) + } + } + + // 初始化交易所 + exchanges := []struct { + id, name, typ string + }{ + {"binance", "Binance", "cex"}, + {"hyperliquid", "Hyperliquid", "dex"}, + } + + for _, exchange := range exchanges { + _, err := d.db.Exec(` + INSERT OR IGNORE INTO exchanges (id, name, type, enabled) + VALUES (?, ?, ?, 0) + `, exchange.id, exchange.name, exchange.typ) + if err != nil { + return fmt.Errorf("初始化交易所失败: %w", err) + } + } + + // 初始化系统配置 + systemConfigs := map[string]string{ + "api_server_port": "8081", + "use_default_coins": "true", + "coin_pool_api_url": "", + "oi_top_api_url": "", + "max_daily_loss": "10.0", + "max_drawdown": "20.0", + "stop_trading_minutes": "60", + } + + for key, value := range systemConfigs { + _, err := d.db.Exec(` + INSERT OR IGNORE INTO system_config (key, value) + VALUES (?, ?) + `, key, value) + if err != nil { + return fmt.Errorf("初始化系统配置失败: %w", err) + } + } + + return nil +} + +// AIModelConfig AI模型配置 +type AIModelConfig struct { + ID string `json:"id"` + Name string `json:"name"` + Provider string `json:"provider"` + Enabled bool `json:"enabled"` + APIKey string `json:"apiKey"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// ExchangeConfig 交易所配置 +type ExchangeConfig struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Enabled bool `json:"enabled"` + APIKey string `json:"apiKey"` + SecretKey string `json:"secretKey"` + Testnet bool `json:"testnet"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// TraderConfig 交易员配置 +type TraderConfig struct { + ID string `json:"id"` + Name string `json:"name"` + AIModelID string `json:"ai_model_id"` + ExchangeID string `json:"exchange_id"` + InitialBalance float64 `json:"initial_balance"` + ScanIntervalMinutes int `json:"scan_interval_minutes"` + IsRunning bool `json:"is_running"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// GetAIModels 获取所有AI模型配置 +func (d *Database) GetAIModels() ([]*AIModelConfig, error) { + rows, err := d.db.Query(` + SELECT id, name, provider, enabled, api_key, created_at, updated_at + FROM ai_models ORDER BY id + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var models []*AIModelConfig + for rows.Next() { + var model AIModelConfig + err := rows.Scan( + &model.ID, &model.Name, &model.Provider, + &model.Enabled, &model.APIKey, + &model.CreatedAt, &model.UpdatedAt, + ) + if err != nil { + return nil, err + } + models = append(models, &model) + } + + return models, nil +} + +// UpdateAIModel 更新AI模型配置 +func (d *Database) UpdateAIModel(id string, enabled bool, apiKey string) error { + _, err := d.db.Exec(` + UPDATE ai_models SET enabled = ?, api_key = ? WHERE id = ? + `, enabled, apiKey, id) + return err +} + +// GetExchanges 获取所有交易所配置 +func (d *Database) GetExchanges() ([]*ExchangeConfig, error) { + rows, err := d.db.Query(` + SELECT id, name, type, enabled, api_key, secret_key, testnet, created_at, updated_at + FROM exchanges ORDER BY id + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var exchanges []*ExchangeConfig + for rows.Next() { + var exchange ExchangeConfig + err := rows.Scan( + &exchange.ID, &exchange.Name, &exchange.Type, + &exchange.Enabled, &exchange.APIKey, &exchange.SecretKey, &exchange.Testnet, + &exchange.CreatedAt, &exchange.UpdatedAt, + ) + if err != nil { + return nil, err + } + exchanges = append(exchanges, &exchange) + } + + return exchanges, nil +} + +// UpdateExchange 更新交易所配置 +func (d *Database) UpdateExchange(id string, enabled bool, apiKey, secretKey string, testnet bool) error { + _, err := d.db.Exec(` + UPDATE exchanges SET enabled = ?, api_key = ?, secret_key = ?, testnet = ? WHERE id = ? + `, enabled, apiKey, secretKey, testnet, id) + return err +} + +// CreateTrader 创建交易员 +func (d *Database) CreateTrader(trader *TraderConfig) error { + _, err := d.db.Exec(` + INSERT INTO traders (id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running) + VALUES (?, ?, ?, ?, ?, ?, ?) + `, trader.ID, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance, trader.ScanIntervalMinutes, trader.IsRunning) + return err +} + +// GetTraders 获取所有交易员 +func (d *Database) GetTraders() ([]*TraderConfig, error) { + rows, err := d.db.Query(` + SELECT id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running, created_at, updated_at + FROM traders ORDER BY created_at DESC + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var traders []*TraderConfig + for rows.Next() { + var trader TraderConfig + err := rows.Scan( + &trader.ID, &trader.Name, &trader.AIModelID, &trader.ExchangeID, + &trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning, + &trader.CreatedAt, &trader.UpdatedAt, + ) + if err != nil { + return nil, err + } + traders = append(traders, &trader) + } + + return traders, nil +} + +// UpdateTraderStatus 更新交易员状态 +func (d *Database) UpdateTraderStatus(id string, isRunning bool) error { + _, err := d.db.Exec(`UPDATE traders SET is_running = ? WHERE id = ?`, isRunning, id) + return err +} + +// DeleteTrader 删除交易员 +func (d *Database) DeleteTrader(id string) error { + _, err := d.db.Exec(`DELETE FROM traders WHERE id = ?`, id) + return err +} + +// GetTraderConfig 获取交易员完整配置(包含AI模型和交易所信息) +func (d *Database) GetTraderConfig(traderID string) (*TraderConfig, *AIModelConfig, *ExchangeConfig, error) { + var trader TraderConfig + var aiModel AIModelConfig + var exchange ExchangeConfig + + err := d.db.QueryRow(` + SELECT + t.id, t.name, t.ai_model_id, t.exchange_id, t.initial_balance, t.scan_interval_minutes, t.is_running, t.created_at, t.updated_at, + a.id, a.name, a.provider, a.enabled, a.api_key, a.created_at, a.updated_at, + e.id, e.name, e.type, e.enabled, e.api_key, e.secret_key, e.testnet, e.created_at, e.updated_at + FROM traders t + JOIN ai_models a ON t.ai_model_id = a.id + JOIN exchanges e ON t.exchange_id = e.id + WHERE t.id = ? + `, traderID).Scan( + &trader.ID, &trader.Name, &trader.AIModelID, &trader.ExchangeID, + &trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning, + &trader.CreatedAt, &trader.UpdatedAt, + &aiModel.ID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey, + &aiModel.CreatedAt, &aiModel.UpdatedAt, + &exchange.ID, &exchange.Name, &exchange.Type, &exchange.Enabled, + &exchange.APIKey, &exchange.SecretKey, &exchange.Testnet, + &exchange.CreatedAt, &exchange.UpdatedAt, + ) + + if err != nil { + return nil, nil, nil, err + } + + return &trader, &aiModel, &exchange, nil +} + +// GetSystemConfig 获取系统配置 +func (d *Database) GetSystemConfig(key string) (string, error) { + var value string + err := d.db.QueryRow(`SELECT value FROM system_config WHERE key = ?`, key).Scan(&value) + return value, err +} + +// SetSystemConfig 设置系统配置 +func (d *Database) SetSystemConfig(key, value string) error { + _, err := d.db.Exec(` + INSERT OR REPLACE INTO system_config (key, value) VALUES (?, ?) + `, key, value) + return err +} + +// Close 关闭数据库连接 +func (d *Database) Close() error { + return d.db.Close() +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 0a64d72c..c063d1e8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: # 后端服务 backend: diff --git a/go.mod b/go.mod index cddc9e3a..0dbb9655 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/adshao/go-binance/v2 v2.8.7 github.com/ethereum/go-ethereum v1.16.5 github.com/gin-gonic/gin v1.11.0 + github.com/mattn/go-sqlite3 v1.14.32 github.com/sonirico/go-hyperliquid v0.17.0 ) diff --git a/go.sum b/go.sum index c30f7b02..eb440888 100644 --- a/go.sum +++ b/go.sum @@ -112,6 +112,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= diff --git a/main.go b/main.go index b956209b..72fc9402 100644 --- a/main.go +++ b/main.go @@ -9,72 +9,87 @@ import ( "nofx/pool" "os" "os/signal" + "strconv" "strings" "syscall" ) func main() { fmt.Println("╔════════════════════════════════════════════════════════════╗") - fmt.Println("║ 🏆 AI模型交易竞赛系统 - Qwen vs DeepSeek ║") + fmt.Println("║ 🤖 AI多模型交易系统 - 支持 DeepSeek & Qwen ║") fmt.Println("╚════════════════════════════════════════════════════════════╝") fmt.Println() - // 加载配置文件 - configFile := "config.json" + // 初始化数据库配置 + dbPath := "config.db" if len(os.Args) > 1 { - configFile = os.Args[1] + dbPath = os.Args[1] } - log.Printf("📋 加载配置文件: %s", configFile) - cfg, err := config.LoadConfig(configFile) + log.Printf("📋 初始化配置数据库: %s", dbPath) + database, err := config.NewDatabase(dbPath) if err != nil { - log.Fatalf("❌ 加载配置失败: %v", err) + log.Fatalf("❌ 初始化数据库失败: %v", err) } + defer database.Close() - log.Printf("✓ 配置加载成功,共%d个trader参赛", len(cfg.Traders)) + // 获取系统配置 + useDefaultCoinsStr, _ := database.GetSystemConfig("use_default_coins") + useDefaultCoins := useDefaultCoinsStr == "true" + apiPortStr, _ := database.GetSystemConfig("api_server_port") + + log.Printf("✓ 配置数据库初始化成功") fmt.Println() // 设置是否使用默认主流币种 - pool.SetUseDefaultCoins(cfg.UseDefaultCoins) - if cfg.UseDefaultCoins { + pool.SetUseDefaultCoins(useDefaultCoins) + if useDefaultCoins { log.Printf("✓ 已启用默认主流币种列表(BTC、ETH、SOL、BNB、XRP、DOGE、ADA、HYPE)") } // 设置币种池API URL - if cfg.CoinPoolAPIURL != "" { - pool.SetCoinPoolAPI(cfg.CoinPoolAPIURL) + coinPoolAPIURL, _ := database.GetSystemConfig("coin_pool_api_url") + if coinPoolAPIURL != "" { + pool.SetCoinPoolAPI(coinPoolAPIURL) log.Printf("✓ 已配置AI500币种池API") } - if cfg.OITopAPIURL != "" { - pool.SetOITopAPI(cfg.OITopAPIURL) + + oiTopAPIURL, _ := database.GetSystemConfig("oi_top_api_url") + if oiTopAPIURL != "" { + pool.SetOITopAPI(oiTopAPIURL) log.Printf("✓ 已配置OI Top API") } // 创建TraderManager traderManager := manager.NewTraderManager() - // 添加所有trader - for i, traderCfg := range cfg.Traders { - log.Printf("📦 [%d/%d] 初始化 %s (%s模型)...", - i+1, len(cfg.Traders), traderCfg.Name, strings.ToUpper(traderCfg.AIModel)) - - err := traderManager.AddTrader( - traderCfg, - cfg.CoinPoolAPIURL, - cfg.MaxDailyLoss, - cfg.MaxDrawdown, - cfg.StopTradingMinutes, - ) - if err != nil { - log.Fatalf("❌ 初始化trader失败: %v", err) - } + // 从数据库加载所有交易员到内存 + err = traderManager.LoadTradersFromDatabase(database) + if err != nil { + log.Fatalf("❌ 加载交易员失败: %v", err) } + // 获取数据库中的所有交易员配置(用于显示) + traders, err := database.GetTraders() + if err != nil { + log.Fatalf("❌ 获取交易员列表失败: %v", err) + } + + // 显示加载的交易员信息 fmt.Println() - fmt.Println("🏁 竞赛参赛者:") - for _, traderCfg := range cfg.Traders { - fmt.Printf(" • %s (%s) - 初始资金: %.0f USDT\n", - traderCfg.Name, strings.ToUpper(traderCfg.AIModel), traderCfg.InitialBalance) + fmt.Println("🤖 数据库中的AI交易员配置:") + if len(traders) == 0 { + fmt.Println(" • 暂无配置的交易员,请通过Web界面创建") + } else { + for _, trader := range traders { + status := "停止" + if trader.IsRunning { + status = "运行中" + } + fmt.Printf(" • %s (%s + %s) - 初始资金: %.0f USDT [%s]\n", + trader.Name, strings.ToUpper(trader.AIModelID), strings.ToUpper(trader.ExchangeID), + trader.InitialBalance, status) + } } fmt.Println() @@ -90,8 +105,16 @@ func main() { fmt.Println(strings.Repeat("=", 60)) fmt.Println() + // 获取API服务器端口 + apiPort := 8081 // 默认端口 + if apiPortStr != "" { + if port, err := strconv.Atoi(apiPortStr); err == nil { + apiPort = port + } + } + // 创建并启动API服务器 - apiServer := api.NewServer(traderManager, cfg.APIServerPort) + apiServer := api.NewServer(traderManager, database, apiPort) go func() { if err := apiServer.Start(); err != nil { log.Printf("❌ API服务器错误: %v", err) @@ -102,8 +125,8 @@ func main() { sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - // 启动所有trader - traderManager.StartAll() + // TODO: 启动数据库中配置为运行状态的交易员 + // traderManager.StartAll() // 等待退出信号 <-sigChan @@ -113,5 +136,5 @@ func main() { traderManager.StopAll() fmt.Println() - fmt.Println("👋 感谢使用AI交易竞赛系统!") + fmt.Println("👋 感谢使用AI交易系统!") } diff --git a/manager/trader_manager.go b/manager/trader_manager.go index 5747572a..65cb92f0 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -5,6 +5,7 @@ import ( "log" "nofx/config" "nofx/trader" + "strconv" "sync" "time" ) @@ -22,44 +23,213 @@ func NewTraderManager() *TraderManager { } } -// AddTrader 添加一个trader -func (tm *TraderManager) AddTrader(cfg config.TraderConfig, coinPoolURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int) error { +// LoadTradersFromDatabase 从数据库加载所有交易员到内存 +func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) error { tm.mu.Lock() defer tm.mu.Unlock() - if _, exists := tm.traders[cfg.ID]; exists { - return fmt.Errorf("trader ID '%s' 已存在", cfg.ID) + // 获取数据库中的所有交易员 + traders, err := database.GetTraders() + if err != nil { + return fmt.Errorf("获取交易员列表失败: %w", err) + } + + log.Printf("📋 加载数据库中的交易员配置: %d 个", len(traders)) + + // 获取系统配置 + coinPoolURL, _ := database.GetSystemConfig("coin_pool_api_url") + maxDailyLossStr, _ := database.GetSystemConfig("max_daily_loss") + maxDrawdownStr, _ := database.GetSystemConfig("max_drawdown") + stopTradingMinutesStr, _ := database.GetSystemConfig("stop_trading_minutes") + + // 解析配置 + maxDailyLoss := 10.0 // 默认值 + if val, err := strconv.ParseFloat(maxDailyLossStr, 64); err == nil { + maxDailyLoss = val + } + + maxDrawdown := 20.0 // 默认值 + if val, err := strconv.ParseFloat(maxDrawdownStr, 64); err == nil { + maxDrawdown = val + } + + stopTradingMinutes := 60 // 默认值 + if val, err := strconv.Atoi(stopTradingMinutesStr); err == nil { + stopTradingMinutes = val + } + + // 为每个交易员获取AI模型和交易所配置 + for _, traderCfg := range traders { + // 获取AI模型配置 + aiModels, err := database.GetAIModels() + if err != nil { + log.Printf("⚠️ 获取AI模型配置失败: %v", err) + continue + } + + var aiModelCfg *config.AIModelConfig + for _, model := range aiModels { + if model.ID == traderCfg.AIModelID { + aiModelCfg = model + break + } + } + + if aiModelCfg == nil { + log.Printf("⚠️ 交易员 %s 的AI模型 %s 不存在,跳过", traderCfg.Name, traderCfg.AIModelID) + continue + } + + if !aiModelCfg.Enabled { + log.Printf("⚠️ 交易员 %s 的AI模型 %s 未启用,跳过", traderCfg.Name, traderCfg.AIModelID) + continue + } + + // 获取交易所配置 + exchanges, err := database.GetExchanges() + if err != nil { + log.Printf("⚠️ 获取交易所配置失败: %v", err) + continue + } + + var exchangeCfg *config.ExchangeConfig + for _, exchange := range exchanges { + if exchange.ID == traderCfg.ExchangeID { + exchangeCfg = exchange + break + } + } + + if exchangeCfg == nil { + log.Printf("⚠️ 交易员 %s 的交易所 %s 不存在,跳过", traderCfg.Name, traderCfg.ExchangeID) + continue + } + + if !exchangeCfg.Enabled { + log.Printf("⚠️ 交易员 %s 的交易所 %s 未启用,跳过", traderCfg.Name, traderCfg.ExchangeID) + continue + } + + // 添加到TraderManager + err = tm.addTraderFromConfig(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, maxDailyLoss, maxDrawdown, stopTradingMinutes) + if err != nil { + log.Printf("❌ 添加交易员 %s 失败: %v", traderCfg.Name, err) + continue + } + } + + log.Printf("✓ 成功加载 %d 个交易员到内存", len(tm.traders)) + return nil +} + +// addTraderFromConfig 内部方法:从配置添加交易员(不加锁,因为调用方已加锁) +func (tm *TraderManager) addTraderFromConfig(traderCfg *config.TraderConfig, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int) error { + if _, exists := tm.traders[traderCfg.ID]; exists { + return fmt.Errorf("trader ID '%s' 已存在", traderCfg.ID) } // 构建AutoTraderConfig traderConfig := trader.AutoTraderConfig{ - ID: cfg.ID, - Name: cfg.Name, - AIModel: cfg.AIModel, - Exchange: cfg.Exchange, - BinanceAPIKey: cfg.BinanceAPIKey, - BinanceSecretKey: cfg.BinanceSecretKey, - HyperliquidPrivateKey: cfg.HyperliquidPrivateKey, - HyperliquidTestnet: cfg.HyperliquidTestnet, + ID: traderCfg.ID, + Name: traderCfg.Name, + AIModel: aiModelCfg.Provider, // 使用provider作为模型标识 + Exchange: exchangeCfg.ID, // 使用exchange ID + BinanceAPIKey: "", + BinanceSecretKey: "", + HyperliquidPrivateKey: "", + HyperliquidTestnet: exchangeCfg.Testnet, CoinPoolAPIURL: coinPoolURL, - UseQwen: cfg.AIModel == "qwen", - DeepSeekKey: cfg.DeepSeekKey, - QwenKey: cfg.QwenKey, - ScanInterval: cfg.GetScanInterval(), - InitialBalance: cfg.InitialBalance, + UseQwen: aiModelCfg.Provider == "qwen", + DeepSeekKey: "", + QwenKey: "", + ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute, + InitialBalance: traderCfg.InitialBalance, MaxDailyLoss: maxDailyLoss, MaxDrawdown: maxDrawdown, StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute, } + // 根据交易所类型设置API密钥 + if exchangeCfg.ID == "binance" { + traderConfig.BinanceAPIKey = exchangeCfg.APIKey + traderConfig.BinanceSecretKey = exchangeCfg.SecretKey + } else if exchangeCfg.ID == "hyperliquid" { + traderConfig.HyperliquidPrivateKey = exchangeCfg.APIKey // hyperliquid用APIKey存储private key + } + + // 根据AI模型设置API密钥 + if aiModelCfg.Provider == "qwen" { + traderConfig.QwenKey = aiModelCfg.APIKey + } else if aiModelCfg.Provider == "deepseek" { + traderConfig.DeepSeekKey = aiModelCfg.APIKey + } + // 创建trader实例 at, err := trader.NewAutoTrader(traderConfig) if err != nil { return fmt.Errorf("创建trader失败: %w", err) } - tm.traders[cfg.ID] = at - log.Printf("✓ Trader '%s' (%s) 已添加", cfg.Name, cfg.AIModel) + tm.traders[traderCfg.ID] = at + log.Printf("✓ Trader '%s' (%s + %s) 已加载到内存", traderCfg.Name, aiModelCfg.Provider, exchangeCfg.ID) + return nil +} + +// AddTrader 从数据库配置添加trader (移除旧版兼容性) + +// AddTraderFromDB 从数据库配置添加trader +func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderConfig, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int) error { + tm.mu.Lock() + defer tm.mu.Unlock() + + if _, exists := tm.traders[traderCfg.ID]; exists { + return fmt.Errorf("trader ID '%s' 已存在", traderCfg.ID) + } + + // 构建AutoTraderConfig + traderConfig := trader.AutoTraderConfig{ + ID: traderCfg.ID, + Name: traderCfg.Name, + AIModel: aiModelCfg.Provider, // 使用provider作为模型标识 + Exchange: exchangeCfg.ID, // 使用exchange ID + BinanceAPIKey: "", + BinanceSecretKey: "", + HyperliquidPrivateKey: "", + HyperliquidTestnet: exchangeCfg.Testnet, + CoinPoolAPIURL: coinPoolURL, + UseQwen: aiModelCfg.Provider == "qwen", + DeepSeekKey: "", + QwenKey: "", + ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute, + InitialBalance: traderCfg.InitialBalance, + MaxDailyLoss: maxDailyLoss, + MaxDrawdown: maxDrawdown, + StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute, + } + + // 根据交易所类型设置API密钥 + if exchangeCfg.ID == "binance" { + traderConfig.BinanceAPIKey = exchangeCfg.APIKey + traderConfig.BinanceSecretKey = exchangeCfg.SecretKey + } else if exchangeCfg.ID == "hyperliquid" { + traderConfig.HyperliquidPrivateKey = exchangeCfg.APIKey // hyperliquid用APIKey存储private key + } + + // 根据AI模型设置API密钥 + if aiModelCfg.Provider == "qwen" { + traderConfig.QwenKey = aiModelCfg.APIKey + } else if aiModelCfg.Provider == "deepseek" { + traderConfig.DeepSeekKey = aiModelCfg.APIKey + } + + // 创建trader实例 + at, err := trader.NewAutoTrader(traderConfig) + if err != nil { + return fmt.Errorf("创建trader失败: %w", err) + } + + tm.traders[traderCfg.ID] = at + log.Printf("✓ Trader '%s' (%s + %s) 已添加", traderCfg.Name, aiModelCfg.Provider, exchangeCfg.ID) return nil } diff --git a/web/.dockerignore b/web/.dockerignore index 2d8c3534..a96f3fde 100644 --- a/web/.dockerignore +++ b/web/.dockerignore @@ -1,6 +1,5 @@ # Dependencies node_modules/ -package-lock.json yarn.lock pnpm-lock.yaml diff --git a/web/Dockerfile b/web/Dockerfile index f7614cad..5219148c 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -8,7 +8,7 @@ WORKDIR /app COPY package*.json ./ # 安装依赖 -RUN npm ci --only=production=false +RUN npm ci # 复制源代码 COPY . . diff --git a/web/src/App.tsx b/web/src/App.tsx index 026c3e2a..dfe418d3 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import useSWR from 'swr'; import { api } from './lib/api'; import { EquityChart } from './components/EquityChart'; -import { CompetitionPage } from './components/CompetitionPage'; +import { AITradersPage } from './components/AITradersPage'; import AILearning from './components/AILearning'; import { LanguageProvider, useLanguage } from './contexts/LanguageContext'; import { t, type Language } from './i18n/translations'; @@ -15,11 +15,11 @@ import type { TraderInfo, } from './types'; -type Page = 'competition' | 'trader'; +type Page = 'traders' | 'trader'; function App() { const { language, setLanguage } = useLanguage(); - const [currentPage, setCurrentPage] = useState('competition'); + const [currentPage, setCurrentPage] = useState('traders'); const [selectedTraderId, setSelectedTraderId] = useState(); const [lastUpdate, setLastUpdate] = useState('--:--:--'); @@ -102,7 +102,8 @@ function App() { {/* Header - Binance Style */}
-
+
+ {/* Left - Logo and Title */}
⚡ @@ -116,30 +117,48 @@ function App() {

-
- {/* GitHub Link */} - { - e.currentTarget.style.background = '#2B3139'; - e.currentTarget.style.color = '#EAECEF'; - e.currentTarget.style.borderColor = '#F0B90B'; - }} - onMouseLeave={(e) => { - e.currentTarget.style.background = '#1E2329'; - e.currentTarget.style.color = '#848E9C'; - e.currentTarget.style.borderColor = '#2B3139'; - }} + + {/* Center - Page Toggle (absolutely positioned) */} +
+ + +
+ + {/* Right - Actions */} +
+ {/* Trader Selector (only show on trader page) */} + {currentPage === 'trader' && traders && traders.length > 0 && ( + + )} {/* Language Toggle */}
@@ -165,48 +184,6 @@ function App() {
- {/* Page Toggle */} -
- - -
- - {/* Trader Selector (only show on trader page) */} - {currentPage === 'trader' && traders && traders.length > 0 && ( - - )} - {/* Status Indicator (only show on trader page) */} {currentPage === 'trader' && status && (
@@ -290,7 +243,6 @@ function TraderDetailsPage({ account, positions, decisions, - stats, lastUpdate, language, }: { @@ -358,9 +310,9 @@ function TraderDetailsPage({ {account && (
- 🔄 Last Update: {lastUpdate} | Total Equity: {account.total_equity.toFixed(2)} | - Available: {account.available_balance.toFixed(2)} | P&L: {account.total_pnl.toFixed(2)}{' '} - ({account.total_pnl_pct.toFixed(2)}%) + 🔄 Last Update: {lastUpdate} | Total Equity: {account?.total_equity?.toFixed(2) || '0.00'} | + Available: {account?.available_balance?.toFixed(2) || '0.00'} | P&L: {account?.total_pnl?.toFixed(2) || '0.00'}{' '} + ({account?.total_pnl_pct?.toFixed(2) || '0.00'}%)
)} @@ -369,20 +321,20 @@ function TraderDetailsPage({
0 : false} + positive={account ? (account.total_pnl || 0) > 0 : false} /> = 0 ? '+' : ''}${account?.total_pnl.toFixed(2) || '0.00'} USDT`} + value={`${(account?.total_pnl || 0) >= 0 ? '+' : ''}${account?.total_pnl?.toFixed(2) || '0.00'} USDT`} change={account?.total_pnl_pct || 0} - positive={account ? account.total_pnl >= 0 : false} + positive={account ? (account.total_pnl || 0) >= 0 : false} /> {/* AI Input Prompt - Collapsible */} - {decision.input_prompt && ( + {(decision as any).input_prompt && (
{showInput && (
- {decision.input_prompt} + {(decision as any).input_prompt}
)}
diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx new file mode 100644 index 00000000..27e0986a --- /dev/null +++ b/web/src/components/AITradersPage.tsx @@ -0,0 +1,998 @@ +import React, { useState, useEffect } from 'react'; +import useSWR from 'swr'; +import { api } from '../lib/api'; +import type { TraderInfo, CreateTraderRequest, AIModel, Exchange } from '../types'; +import { useLanguage } from '../contexts/LanguageContext'; +import { t } from '../i18n/translations'; + +export function AITradersPage() { + const { language } = useLanguage(); + const [showCreateModal, setShowCreateModal] = useState(false); + const [showModelModal, setShowModelModal] = useState(false); + const [showExchangeModal, setShowExchangeModal] = useState(false); + const [editingModel, setEditingModel] = useState(null); + const [editingExchange, setEditingExchange] = useState(null); + const [allModels, setAllModels] = useState([]); + const [allExchanges, setAllExchanges] = useState([]); + + const { data: traders, mutate: mutateTraders } = useSWR( + 'traders', + api.getTraders, + { refreshInterval: 5000 } + ); + + // 加载AI模型和交易所配置 + useEffect(() => { + const loadConfigs = async () => { + try { + const [modelConfigs, exchangeConfigs] = await Promise.all([ + api.getModelConfigs(), + api.getExchangeConfigs() + ]); + setAllModels(modelConfigs); + setAllExchanges(exchangeConfigs); + } catch (error) { + console.error('Failed to load configs:', error); + } + }; + loadConfigs(); + }, []); + + // 只显示已配置的模型和交易所 + const configuredModels = allModels.filter(m => m.enabled && m.apiKey); + const configuredExchanges = allExchanges.filter(e => e.enabled && e.apiKey && (e.id === 'hyperliquid' || e.secretKey)); + + // 检查模型是否正在被运行中的交易员使用 + const isModelInUse = (modelId: string) => { + return traders?.some(t => t.ai_model === modelId && t.is_running) || false; + }; + + // 检查交易所是否正在被运行中的交易员使用 + const isExchangeInUse = (exchangeId: string) => { + return traders?.some(t => t.exchange_id === exchangeId && t.is_running) || false; + }; + + const handleCreateTrader = async (modelId: string, exchangeId: string, name: string, initialBalance: number) => { + try { + const model = allModels.find(m => m.id === modelId); + const exchange = allExchanges.find(e => e.id === exchangeId); + + if (!model?.enabled) { + alert(t('modelNotConfigured', language)); + return; + } + + if (!exchange?.enabled) { + alert(t('exchangeNotConfigured', language)); + return; + } + + const request: CreateTraderRequest = { + name, + ai_model_id: modelId, + exchange_id: exchangeId, + initial_balance: initialBalance + }; + + await api.createTrader(request); + setShowCreateModal(false); + mutateTraders(); + } catch (error) { + console.error('Failed to create trader:', error); + alert('创建交易员失败'); + } + }; + + const handleDeleteTrader = async (traderId: string) => { + if (!confirm(t('confirmDeleteTrader', language))) return; + + try { + await api.deleteTrader(traderId); + mutateTraders(); + } catch (error) { + console.error('Failed to delete trader:', error); + alert('删除交易员失败'); + } + }; + + const handleToggleTrader = async (traderId: string, running: boolean) => { + try { + if (running) { + await api.stopTrader(traderId); + } else { + await api.startTrader(traderId); + } + mutateTraders(); + } catch (error) { + console.error('Failed to toggle trader:', error); + alert('操作失败'); + } + }; + + const handleModelClick = (modelId: string) => { + if (!isModelInUse(modelId)) { + setEditingModel(modelId); + setShowModelModal(true); + } + }; + + const handleExchangeClick = (exchangeId: string) => { + if (!isExchangeInUse(exchangeId)) { + setEditingExchange(exchangeId); + setShowExchangeModal(true); + } + }; + + const handleDeleteModelConfig = async (modelId: string) => { + if (!confirm('确定要删除此AI模型配置吗?')) return; + + try { + const updatedModels = allModels.map(m => + m.id === modelId ? { ...m, apiKey: '', enabled: false } : m + ); + + const request = { + models: Object.fromEntries( + updatedModels.map(model => [ + model.id, + { + enabled: model.enabled, + api_key: model.apiKey || '' + } + ]) + ) + }; + + await api.updateModelConfigs(request); + setAllModels(updatedModels); + setShowModelModal(false); + setEditingModel(null); + } catch (error) { + console.error('Failed to delete model config:', error); + alert('删除配置失败'); + } + }; + + const handleSaveModelConfig = async (modelId: string, apiKey: string) => { + try { + const updatedModels = allModels.map(m => + m.id === modelId ? { ...m, apiKey, enabled: true } : m + ); + + const request = { + models: Object.fromEntries( + updatedModels.map(model => [ + model.id, + { + enabled: model.enabled, + api_key: model.apiKey || '' + } + ]) + ) + }; + + await api.updateModelConfigs(request); + setAllModels(updatedModels); + setShowModelModal(false); + setEditingModel(null); + } catch (error) { + console.error('Failed to save model config:', error); + alert('保存配置失败'); + } + }; + + const handleDeleteExchangeConfig = async (exchangeId: string) => { + if (!confirm('确定要删除此交易所配置吗?')) return; + + try { + const updatedExchanges = allExchanges.map(e => + e.id === exchangeId ? { ...e, apiKey: '', secretKey: '', enabled: false } : e + ); + + const request = { + exchanges: Object.fromEntries( + updatedExchanges.map(exchange => [ + exchange.id, + { + enabled: exchange.enabled, + api_key: exchange.apiKey || '', + secret_key: exchange.secretKey || '', + testnet: exchange.testnet || false + } + ]) + ) + }; + + await api.updateExchangeConfigs(request); + setAllExchanges(updatedExchanges); + setShowExchangeModal(false); + setEditingExchange(null); + } catch (error) { + console.error('Failed to delete exchange config:', error); + alert('删除交易所配置失败'); + } + }; + + const handleSaveExchangeConfig = async (exchangeId: string, apiKey: string, secretKey?: string, testnet?: boolean) => { + try { + const updatedExchanges = allExchanges.map(e => + e.id === exchangeId ? { ...e, apiKey, secretKey, testnet, enabled: true } : e + ); + + const request = { + exchanges: Object.fromEntries( + updatedExchanges.map(exchange => [ + exchange.id, + { + enabled: exchange.enabled, + api_key: exchange.apiKey || '', + secret_key: exchange.secretKey || '', + testnet: exchange.testnet || false + } + ]) + ) + }; + + await api.updateExchangeConfigs(request); + setAllExchanges(updatedExchanges); + setShowExchangeModal(false); + setEditingExchange(null); + } catch (error) { + console.error('Failed to save exchange config:', error); + alert('保存交易所配置失败'); + } + }; + + const handleAddModel = () => { + setEditingModel(null); + setShowModelModal(true); + }; + + const handleAddExchange = () => { + setEditingExchange(null); + setShowExchangeModal(true); + }; + + return ( +
+ {/* Header */} +
+
+
+ 🤖 +
+
+

+ {t('aiTraders', language)} + + {traders?.length || 0} {t('active', language)} + +

+

+ {t('manageAITraders', language)} +

+
+
+ +
+ + + + + +
+
+ + {/* Configuration Status */} +
+ {/* AI Models */} +
+

+ 🧠 {t('aiModels', language)} +

+
+ {configuredModels.map(model => { + const inUse = isModelInUse(model.id); + return ( +
handleModelClick(model.id)} + > +
+
+ {model.name[0]} +
+
+
{model.name}
+
+ {t('configured', language)} +
+
+
+
+
+ ); + })} + {configuredModels.length === 0 && ( +
+
🧠
+
暂无已配置的AI模型
+
+ )} +
+
+ + {/* Exchanges */} +
+

+ 🏦 {t('exchanges', language)} +

+
+ {configuredExchanges.map(exchange => { + const inUse = isExchangeInUse(exchange.id); + return ( +
handleExchangeClick(exchange.id)} + > +
+
+ {exchange.name[0]} +
+
+
{exchange.name}
+
+ {exchange.type.toUpperCase()} • {t('configured', language)} +
+
+
+
+
+ ); + })} + {configuredExchanges.length === 0 && ( +
+
🏦
+
暂无已配置的交易所
+
+ )} +
+
+
+ + {/* Traders List */} +
+
+

+ 👥 {t('currentTraders', language)} +

+
+ + {traders && traders.length > 0 ? ( +
+ {traders.map(trader => ( +
+
+
+ 🤖 +
+
+
+ {trader.trader_name} +
+
+ {trader.ai_model.toUpperCase()} Model • {trader.exchange_id?.toUpperCase()} +
+
+
+ +
+ {/* Status */} +
+
{t('status', language)}
+
+ {trader.is_running ? t('running', language) : t('stopped', language)} +
+
+ + {/* Actions */} +
+ + + +
+
+
+ ))} +
+ ) : ( +
+
🤖
+
{t('noTraders', language)}
+
{t('createFirstTrader', language)}
+ {(configuredModels.length === 0 || configuredExchanges.length === 0) && ( +
+ {configuredModels.length === 0 && configuredExchanges.length === 0 + ? t('configureModelsAndExchangesFirst', language) + : configuredModels.length === 0 + ? t('configureModelsFirst', language) + : t('configureExchangesFirst', language) + } +
+ )} +
+ )} +
+ + {/* Create Trader Modal */} + {showCreateModal && ( + setShowCreateModal(false)} + language={language} + /> + )} + + {/* Model Configuration Modal */} + {showModelModal && ( + { + setShowModelModal(false); + setEditingModel(null); + }} + language={language} + /> + )} + + {/* Exchange Configuration Modal */} + {showExchangeModal && ( + { + setShowExchangeModal(false); + setEditingExchange(null); + }} + language={language} + /> + )} +
+ ); +} + +// Create Trader Modal Component +function CreateTraderModal({ + enabledModels, + enabledExchanges, + onCreate, + onClose, + language +}: { + enabledModels: AIModel[]; + enabledExchanges: Exchange[]; + onCreate: (modelId: string, exchangeId: string, name: string, initialBalance: number) => void; + onClose: () => void; + language: any; +}) { + // 默认选择DeepSeek模型,如果没有启用则选择第一个 + const defaultModel = enabledModels.find(m => m.id === 'deepseek') || enabledModels[0]; + // 默认选择Binance交易所,如果没有启用则选择第一个 + const defaultExchange = enabledExchanges.find(e => e.id === 'binance') || enabledExchanges[0]; + + const [selectedModel, setSelectedModel] = useState(defaultModel?.id || ''); + const [selectedExchange, setSelectedExchange] = useState(defaultExchange?.id || ''); + const [traderName, setTraderName] = useState(''); + const [initialBalance, setInitialBalance] = useState(1000); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedModel || !selectedExchange || !traderName.trim()) return; + + onCreate(selectedModel, selectedExchange, traderName.trim(), initialBalance); + }; + + return ( +
+
+

+ {t('createNewTrader', language)} +

+ +
+
+ + +
+ +
+ + setTraderName(e.target.value)} + placeholder={t('enterTraderName', language)} + className="w-full px-3 py-2 rounded" + style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} + required + /> +
+ +
+ + +
+ +
+ + setInitialBalance(Number(e.target.value))} + min="100" + max="100000" + className="w-full px-3 py-2 rounded" + style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} + required + /> +
+ +
+ + +
+
+
+
+ ); +} + +// Model Configuration Modal Component +function ModelConfigModal({ + allModels, + editingModelId, + onSave, + onDelete, + onClose, + language +}: { + allModels: AIModel[]; + editingModelId: string | null; + onSave: (modelId: string, apiKey: string) => void; + onDelete: (modelId: string) => void; + onClose: () => void; + language: any; +}) { + const [selectedModelId, setSelectedModelId] = useState(editingModelId || ''); + const [apiKey, setApiKey] = useState(''); + + // 获取当前编辑的模型信息 + const selectedModel = allModels.find(m => m.id === selectedModelId); + + // 如果是编辑现有模型,初始化API Key + useEffect(() => { + if (editingModelId && selectedModel) { + setApiKey(selectedModel.apiKey || ''); + } + }, [editingModelId, selectedModel]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedModelId || !apiKey.trim()) return; + + onSave(selectedModelId, apiKey.trim()); + }; + + // 可选择的模型列表(排除已配置的,除非是当前编辑的) + const availableModels = allModels.filter(m => + !m.enabled || !m.apiKey || m.id === editingModelId + ); + + return ( +
+
+

+ {editingModelId ? '编辑AI模型' : '添加AI模型'} +

+ +
+ {!editingModelId && ( +
+ + +
+ )} + + {selectedModel && ( +
+
+
+ {selectedModel.name[0]} +
+
+
{selectedModel.name}
+
{selectedModel.provider}
+
+
+ +
+ + setApiKey(e.target.value)} + placeholder={`请输入 ${selectedModel.name} API Key`} + className="w-full px-3 py-2 rounded" + style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }} + required + /> +
+
+ )} + +
+ + {editingModelId && ( + + )} + +
+
+
+
+ ); +} + +// Exchange Configuration Modal Component +function ExchangeConfigModal({ + allExchanges, + editingExchangeId, + onSave, + onDelete, + onClose, + language +}: { + allExchanges: Exchange[]; + editingExchangeId: string | null; + onSave: (exchangeId: string, apiKey: string, secretKey?: string, testnet?: boolean) => void; + onDelete: (exchangeId: string) => void; + onClose: () => void; + language: any; +}) { + const [selectedExchangeId, setSelectedExchangeId] = useState(editingExchangeId || ''); + const [apiKey, setApiKey] = useState(''); + const [secretKey, setSecretKey] = useState(''); + const [testnet, setTestnet] = useState(false); + + // 获取当前编辑的交易所信息 + const selectedExchange = allExchanges.find(e => e.id === selectedExchangeId); + + // 如果是编辑现有交易所,初始化表单数据 + useEffect(() => { + if (editingExchangeId && selectedExchange) { + setApiKey(selectedExchange.apiKey || ''); + setSecretKey(selectedExchange.secretKey || ''); + setTestnet(selectedExchange.testnet || false); + } + }, [editingExchangeId, selectedExchange]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedExchangeId || !apiKey.trim()) return; + if (selectedExchange?.id !== 'hyperliquid' && !secretKey.trim()) return; + + onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet); + }; + + // 可选择的交易所列表(排除已配置的,除非是当前编辑的) + const availableExchanges = allExchanges.filter(e => + !e.enabled || !e.apiKey || e.id === editingExchangeId + ); + + return ( +
+
+

+ {editingExchangeId ? '编辑交易所' : '添加交易所'} +

+ +
+ {!editingExchangeId && ( +
+ + +
+ )} + + {selectedExchange && ( +
+
+
+ {selectedExchange.name[0]} +
+
+
{selectedExchange.name}
+
{selectedExchange.type.toUpperCase()}
+
+
+ +
+
+ + setApiKey(e.target.value)} + placeholder={selectedExchange.id === 'hyperliquid' ? '请输入以太坊私钥' : `请输入 ${selectedExchange.name} API Key`} + className="w-full px-3 py-2 rounded" + style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }} + required + /> +
+ + {selectedExchange.id !== 'hyperliquid' && ( +
+ + setSecretKey(e.target.value)} + placeholder={`请输入 ${selectedExchange.name} Secret Key`} + className="w-full px-3 py-2 rounded" + style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }} + required + /> +
+ )} + + {selectedExchange.type === 'dex' && ( +
+ setTestnet(e.target.checked)} + className="w-4 h-4" + /> + +
+ )} +
+
+ )} + +
+ + {editingExchangeId && ( + + )} + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/web/src/components/ComparisonChart.tsx b/web/src/components/ComparisonChart.tsx deleted file mode 100644 index 3fdc9dcc..00000000 --- a/web/src/components/ComparisonChart.tsx +++ /dev/null @@ -1,336 +0,0 @@ -import { useState, useEffect, useMemo } from 'react'; -import { - LineChart, - Line, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, - ReferenceLine, - Legend, -} from 'recharts'; -import useSWR from 'swr'; -import { api } from '../lib/api'; -import type { CompetitionTraderData } from '../types'; - -interface ComparisonChartProps { - traders: CompetitionTraderData[]; -} - -export function ComparisonChart({ traders }: ComparisonChartProps) { - // 获取所有trader的历史数据 - const traderHistories = traders.map((trader) => { - // eslint-disable-next-line react-hooks/rules-of-hooks - return useSWR(`equity-history-${trader.trader_id}`, () => - api.getEquityHistory(trader.trader_id), - { refreshInterval: 10000 } - ); - }); - - // 使用useMemo自动处理数据合并,直接使用data对象作为依赖 - const combinedData = useMemo(() => { - // 等待所有数据加载完成 - const allLoaded = traderHistories.every((h) => h.data); - if (!allLoaded) return []; - - console.log(`[${new Date().toISOString()}] Recalculating chart data...`); - - // 新方案:按时间戳分组,不再依赖 cycle_number(因为后端会重置) - // 收集所有时间戳 - const timestampMap = new Map; - }>(); - - traderHistories.forEach((history, index) => { - const trader = traders[index]; - if (!history.data) return; - - console.log(`Trader ${trader.trader_id}: ${history.data.length} data points`); - - history.data.forEach((point: any) => { - const ts = point.timestamp; - - if (!timestampMap.has(ts)) { - const time = new Date(ts).toLocaleTimeString('zh-CN', { - hour: '2-digit', - minute: '2-digit', - }); - timestampMap.set(ts, { - timestamp: ts, - time, - traders: new Map() - }); - } - - timestampMap.get(ts)!.traders.set(trader.trader_id, { - pnl_pct: point.total_pnl_pct, - equity: point.total_equity - }); - }); - }); - - // 按时间戳排序,转换为数组 - const combined = Array.from(timestampMap.entries()) - .sort(([tsA], [tsB]) => new Date(tsA).getTime() - new Date(tsB).getTime()) - .map(([ts, data], index) => { - const entry: any = { - index: index + 1, // 使用序号代替cycle - time: data.time, - timestamp: ts - }; - - traders.forEach((trader) => { - const traderData = data.traders.get(trader.trader_id); - if (traderData) { - entry[`${trader.trader_id}_pnl_pct`] = traderData.pnl_pct; - entry[`${trader.trader_id}_equity`] = traderData.equity; - } - }); - - return entry; - }); - - if (combined.length > 0) { - const lastPoint = combined[combined.length - 1]; - console.log(`Chart: ${combined.length} data points, last time: ${lastPoint.time}, timestamp: ${lastPoint.timestamp}`); - console.log('Last 3 points:', combined.slice(-3).map(p => ({ - time: p.time, - timestamp: p.timestamp, - deepseek: p.deepseek_trader_pnl_pct, - qwen: p.qwen_trader_pnl_pct - }))); - } - - return combined; - }, [ - traderHistories[0]?.data, - traderHistories[1]?.data, - ]); - - const isLoading = traderHistories.some((h) => !h.data); - - if (isLoading) { - return ( -
-
-
Loading comparison data...
-
- ); - } - - if (combinedData.length === 0) { - return ( -
-
📊
-
暂无历史数据
-
运行几个周期后将显示对比曲线
-
- ); - } - - // 限制显示数据点 - const MAX_DISPLAY_POINTS = 2000; - const displayData = - combinedData.length > MAX_DISPLAY_POINTS - ? combinedData.slice(-MAX_DISPLAY_POINTS) - : combinedData; - - // 计算Y轴范围 - const calculateYDomain = () => { - const allValues: number[] = []; - displayData.forEach((point) => { - traders.forEach((trader) => { - const value = point[`${trader.trader_id}_pnl_pct`]; - if (value !== undefined) { - allValues.push(value); - } - }); - }); - - if (allValues.length === 0) return [-5, 5]; - - const minVal = Math.min(...allValues); - const maxVal = Math.max(...allValues); - const range = Math.max(Math.abs(maxVal), Math.abs(minVal)); - const padding = Math.max(range * 0.2, 1); // 至少留1%余量 - - return [ - Math.floor(minVal - padding), - Math.ceil(maxVal + padding) - ]; - }; - - // Trader颜色配置 - 使用更鲜艳对比度更高的颜色 - const getTraderColor = (traderId: string) => { - const trader = traders.find((t) => t.trader_id === traderId); - if (trader?.ai_model === 'qwen') { - return '#c084fc'; // purple-400 (更亮) - } else { - return '#60a5fa'; // blue-400 (更亮) - } - }; - - // 自定义Tooltip - Binance Style - const CustomTooltip = ({ active, payload }: any) => { - if (active && payload && payload.length) { - const data = payload[0].payload; - return ( -
-
- {data.time} - #{data.index} -
- {traders.map((trader) => { - const pnlPct = data[`${trader.trader_id}_pnl_pct`]; - const equity = data[`${trader.trader_id}_equity`]; - if (pnlPct === undefined) return null; - - return ( -
-
- {trader.trader_name} -
-
= 0 ? '#0ECB81' : '#F6465D' }}> - {pnlPct >= 0 ? '+' : ''}{pnlPct.toFixed(2)}% - - ({equity?.toFixed(2)} USDT) - -
-
- ); - })} -
- ); - } - return null; - }; - - // 计算当前差距 - const currentGap = displayData.length > 0 ? (() => { - const lastPoint = displayData[displayData.length - 1]; - const values = traders.map(t => lastPoint[`${t.trader_id}_pnl_pct`] || 0); - return Math.abs(values[0] - values[1]); - })() : 0; - - return ( -
-
- - - - {traders.map((trader) => ( - - - - - ))} - - - - - - - `${value.toFixed(1)}%`} - width={60} - /> - - } /> - - - - {traders.map((trader, index) => ( - - ))} - - { - const traderId = traders.find((t) => value === t.trader_name)?.trader_id; - const trader = traders.find((t) => t.trader_id === traderId); - return ( - - {trader?.trader_name} ({trader?.ai_model.toUpperCase()}) - - ); - }} - /> - - -
- - {/* Stats */} -
-
-
对比模式
-
PnL %
-
-
-
数据点数
-
{combinedData.length} 个
-
-
-
当前差距
-
1 ? '#F0B90B' : '#EAECEF' }}> - {currentGap.toFixed(2)}% -
-
-
-
显示范围
-
- {combinedData.length > MAX_DISPLAY_POINTS - ? `最近 ${MAX_DISPLAY_POINTS}` - : '全部数据'} -
-
-
-
- ); -} diff --git a/web/src/components/CompetitionPage.tsx b/web/src/components/CompetitionPage.tsx deleted file mode 100644 index a72f6dfb..00000000 --- a/web/src/components/CompetitionPage.tsx +++ /dev/null @@ -1,251 +0,0 @@ -import useSWR from 'swr'; -import { api } from '../lib/api'; -import type { CompetitionData } from '../types'; -import { ComparisonChart } from './ComparisonChart'; -import { useLanguage } from '../contexts/LanguageContext'; -import { t } from '../i18n/translations'; - -export function CompetitionPage() { - const { language } = useLanguage(); - const { data: competition } = useSWR( - 'competition', - api.getCompetition, - { - refreshInterval: 5000, - revalidateOnFocus: true, - } - ); - - if (!competition || !competition.traders) { - return ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ); - } - - // 按收益率排序 - const sortedTraders = [...competition.traders].sort( - (a, b) => b.total_pnl_pct - a.total_pnl_pct - ); - - // 找出领先者 - const leader = sortedTraders[0]; - - return ( -
- {/* Competition Header - 精简版 */} -
-
-
- 🏆 -
-
-

- {t('aiCompetition', language)} - - {competition.count} {t('traders', language)} - -

-

- {t('liveBattle', language)} -

-
-
-
-
{t('leader', language)}
-
{leader?.trader_name}
-
= 0 ? '#0ECB81' : '#F6465D' }}> - {leader.total_pnl >= 0 ? '+' : ''}{leader.total_pnl_pct.toFixed(2)}% -
-
-
- - {/* Left/Right Split: Performance Chart + Leaderboard */} -
- {/* Left: Performance Comparison Chart */} -
-
-

- {t('performanceComparison', language)} -

-
- {t('realTimePnL', language)} -
-
- -
- - {/* Right: Leaderboard */} -
-
-

- {t('leaderboard', language)} -

-
- {t('live', language)} -
-
-
- {sortedTraders.map((trader, index) => { - const isLeader = index === 0; - const aiModelColor = trader.ai_model === 'qwen' ? '#c084fc' : '#60a5fa'; - - return ( -
-
- {/* Rank & Name */} -
-
- {index === 0 ? '🥇' : index === 1 ? '🥈' : '🥉'} -
-
-
{trader.trader_name}
-
- {trader.ai_model.toUpperCase()} -
-
-
- - {/* Stats */} -
- {/* Total Equity */} -
-
{t('equity', language)}
-
- {trader.total_equity.toFixed(2)} -
-
- - {/* P&L */} -
-
{t('pnl', language)}
-
= 0 ? '#0ECB81' : '#F6465D' }} - > - {trader.total_pnl >= 0 ? '+' : ''} - {trader.total_pnl_pct.toFixed(2)}% -
-
- {trader.total_pnl >= 0 ? '+' : ''}{trader.total_pnl.toFixed(2)} -
-
- - {/* Positions */} -
-
{t('pos', language)}
-
- {trader.position_count} -
-
- {trader.margin_used_pct.toFixed(1)}% -
-
- - {/* Status */} -
-
- {trader.is_running ? '●' : '○'} -
-
-
-
-
- ); - })} -
-
-
- - {/* Head-to-Head Stats */} - {competition.traders.length === 2 && ( -
-

- {t('headToHead', language)} -

-
- {sortedTraders.map((trader, index) => { - const isWinning = index === 0; - const opponent = sortedTraders[1 - index]; - const gap = trader.total_pnl_pct - opponent.total_pnl_pct; - - return ( -
-
-
- {trader.trader_name} -
-
= 0 ? '#0ECB81' : '#F6465D' }}> - {trader.total_pnl >= 0 ? '+' : ''}{trader.total_pnl_pct.toFixed(2)}% -
- {isWinning && gap > 0 && ( -
- {t('leadingBy', language, { gap: gap.toFixed(2) })} -
- )} - {!isWinning && gap < 0 && ( -
- {t('behindBy', language, { gap: Math.abs(gap).toFixed(2) })} -
- )} -
-
- ); - })} -
-
- )} -
- ); -} diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts index 9f43ef4e..c5682568 100644 --- a/web/src/i18n/translations.ts +++ b/web/src/i18n/translations.ts @@ -3,15 +3,16 @@ export type Language = 'en' | 'zh'; export const translations = { en: { // Header - appTitle: 'AI Trading Competition', - subtitle: 'Qwen vs DeepSeek · Real-time', - competition: 'Competition', + appTitle: 'AI Trading System', + subtitle: 'Multi-AI Model Trading Platform', + aiTraders: 'AI Traders', details: 'Details', + tradingPanel: 'Trading Panel', running: 'RUNNING', stopped: 'STOPPED', // Footer - footerTitle: 'NOFX - AI Trading Competition System', + footerTitle: 'NOFX - AI Trading System', footerWarning: '⚠️ Trading involves risk. Use at your own discretion.', // Stats Cards @@ -114,6 +115,39 @@ export const translations = { aiLearningPoint3: 'Optimizes position sizing based on win rate', aiLearningPoint4: 'Avoids repeating past mistakes', + // AI Traders Management + manageAITraders: 'Manage your AI trading bots', + aiModels: 'AI Models', + exchanges: 'Exchanges', + createTrader: 'Create Trader', + modelConfiguration: 'Model Configuration', + configured: 'Configured', + notConfigured: 'Not Configured', + currentTraders: 'Current Traders', + noTraders: 'No AI Traders', + createFirstTrader: 'Create your first AI trader to get started', + configureModelsFirst: 'Please configure AI models first', + configureExchangesFirst: 'Please configure exchanges first', + configureModelsAndExchangesFirst: 'Please configure AI models and exchanges first', + modelNotConfigured: 'Selected model is not configured', + exchangeNotConfigured: 'Selected exchange is not configured', + confirmDeleteTrader: 'Are you sure you want to delete this trader?', + status: 'Status', + start: 'Start', + stop: 'Stop', + createNewTrader: 'Create New AI Trader', + selectAIModel: 'Select AI Model', + selectExchange: 'Select Exchange', + traderName: 'Trader Name', + enterTraderName: 'Enter trader name', + cancel: 'Cancel', + create: 'Create', + configureAIModels: 'Configure AI Models', + configureExchanges: 'Configure Exchanges', + useTestnet: 'Use Testnet', + enabled: 'Enabled', + save: 'Save', + // Loading & Error loading: 'Loading...', loadingError: '⚠️ Failed to load AI learning data', @@ -121,15 +155,16 @@ export const translations = { }, zh: { // Header - appTitle: 'AI交易竞赛', - subtitle: 'Qwen vs DeepSeek · 实时', - competition: '竞赛', + appTitle: 'AI交易系统', + subtitle: '多AI模型交易平台', + aiTraders: 'AI交易员', details: '详情', + tradingPanel: '交易面板', running: '运行中', stopped: '已停止', // Footer - footerTitle: 'NOFX - AI交易竞赛系统', + footerTitle: 'NOFX - AI交易系统', footerWarning: '⚠️ 交易有风险,请谨慎使用。', // Stats Cards @@ -232,6 +267,39 @@ export const translations = { aiLearningPoint3: '根据胜率优化仓位大小', aiLearningPoint4: '避免重复过去的错误', + // AI Traders Management + manageAITraders: '管理您的AI交易机器人', + aiModels: 'AI模型', + exchanges: '交易所', + createTrader: '创建交易员', + modelConfiguration: '模型配置', + configured: '已配置', + notConfigured: '未配置', + currentTraders: '当前交易员', + noTraders: '暂无AI交易员', + createFirstTrader: '创建您的第一个AI交易员开始使用', + configureModelsFirst: '请先配置AI模型', + configureExchangesFirst: '请先配置交易所', + configureModelsAndExchangesFirst: '请先配置AI模型和交易所', + modelNotConfigured: '所选模型未配置', + exchangeNotConfigured: '所选交易所未配置', + confirmDeleteTrader: '确定要删除这个交易员吗?', + status: '状态', + start: '启动', + stop: '停止', + createNewTrader: '创建新的AI交易员', + selectAIModel: '选择AI模型', + selectExchange: '选择交易所', + traderName: '交易员名称', + enterTraderName: '输入交易员名称', + cancel: '取消', + create: '创建', + configureAIModels: '配置AI模型', + configureExchanges: '配置交易所', + useTestnet: '使用测试网', + enabled: '启用', + save: '保存', + // Loading & Error loading: '加载中...', loadingError: '⚠️ 加载AI学习数据失败', diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 22d1b82d..d457755d 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -5,25 +5,86 @@ import type { DecisionRecord, Statistics, TraderInfo, - CompetitionData, + AIModel, + Exchange, + CreateTraderRequest, + UpdateModelConfigRequest, + UpdateExchangeConfigRequest, } from '../types'; const API_BASE = '/api'; export const api = { - // 竞赛相关接口 - async getCompetition(): Promise { - const res = await fetch(`${API_BASE}/competition`); - if (!res.ok) throw new Error('获取竞赛数据失败'); - return res.json(); - }, - + // AI交易员管理接口 async getTraders(): Promise { const res = await fetch(`${API_BASE}/traders`); if (!res.ok) throw new Error('获取trader列表失败'); return res.json(); }, + async createTrader(request: CreateTraderRequest): Promise { + const res = await fetch(`${API_BASE}/traders`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(request), + }); + if (!res.ok) throw new Error('创建交易员失败'); + return res.json(); + }, + + async deleteTrader(traderId: string): Promise { + const res = await fetch(`${API_BASE}/traders/${traderId}`, { + method: 'DELETE', + }); + if (!res.ok) throw new Error('删除交易员失败'); + }, + + async startTrader(traderId: string): Promise { + const res = await fetch(`${API_BASE}/traders/${traderId}/start`, { + method: 'POST', + }); + if (!res.ok) throw new Error('启动交易员失败'); + }, + + async stopTrader(traderId: string): Promise { + const res = await fetch(`${API_BASE}/traders/${traderId}/stop`, { + method: 'POST', + }); + if (!res.ok) throw new Error('停止交易员失败'); + }, + + // AI模型配置接口 + async getModelConfigs(): Promise { + const res = await fetch(`${API_BASE}/models`); + if (!res.ok) throw new Error('获取模型配置失败'); + return res.json(); + }, + + async updateModelConfigs(request: UpdateModelConfigRequest): Promise { + const res = await fetch(`${API_BASE}/models`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(request), + }); + if (!res.ok) throw new Error('更新模型配置失败'); + }, + + // 交易所配置接口 + async getExchangeConfigs(): Promise { + const res = await fetch(`${API_BASE}/exchanges`); + if (!res.ok) throw new Error('获取交易所配置失败'); + return res.json(); + }, + + async updateExchangeConfigs(request: UpdateExchangeConfigRequest): Promise { + const res = await fetch(`${API_BASE}/exchanges`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(request), + }); + if (!res.ok) throw new Error('更新交易所配置失败'); + }, + // 获取系统状态(支持trader_id) async getStatus(traderId?: string): Promise { const url = traderId diff --git a/web/src/types.ts b/web/src/types.ts index c61eca80..90d1e94f 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -83,27 +83,55 @@ export interface Statistics { total_close_positions: number; } -// 新增:竞赛相关类型 +// AI Trading相关类型 export interface TraderInfo { trader_id: string; trader_name: string; ai_model: string; + is_running?: boolean; } -export interface CompetitionTraderData { - trader_id: string; - trader_name: string; - ai_model: string; - total_equity: number; - total_pnl: number; - total_pnl_pct: number; - position_count: number; - margin_used_pct: number; - call_count: number; - is_running: boolean; +export interface AIModel { + id: string; + name: string; + provider: string; + enabled: boolean; + apiKey?: string; } -export interface CompetitionData { - traders: CompetitionTraderData[]; - count: number; +export interface Exchange { + id: string; + name: string; + type: 'cex' | 'dex'; + enabled: boolean; + apiKey?: string; + secretKey?: string; + testnet?: boolean; +} + +export interface CreateTraderRequest { + name: string; + ai_model_id: string; + exchange_id: string; + initial_balance: number; +} + +export interface UpdateModelConfigRequest { + models: { + [key: string]: { + enabled: boolean; + api_key: string; + }; + }; +} + +export interface UpdateExchangeConfigRequest { + exchanges: { + [key: string]: { + enabled: boolean; + api_key: string; + secret_key: string; + testnet?: boolean; + }; + }; } diff --git a/web/vite.config.ts b/web/vite.config.ts index 3e35b475..576ccf6c 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -7,7 +7,7 @@ export default defineConfig({ port: 3000, proxy: { '/api': { - target: 'http://localhost:8080', + target: 'http://localhost:8081', changeOrigin: true, }, }, From c9f9f4318e390e2a99d07c2dfcb2e10e0e73eec3 Mon Sep 17 00:00:00 2001 From: Yinghao Fan Date: Fri, 31 Oct 2025 02:06:20 +0800 Subject: [PATCH 02/67] fix: Correct error handling in decision parsing Changes: - Updated error handling in `GetFullDecision` and `parseFullDecisionResponse` functions to return the decision object even when an error occurs, improving the clarity of error messages. This ensures that the decision object is consistently returned, allowing for better debugging and handling of errors in the decision-making process. --- decision/engine.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/decision/engine.go b/decision/engine.go index 76bcffca..97181572 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -109,7 +109,7 @@ func GetFullDecision(ctx *Context, mcpClient *mcp.Client) (*FullDecision, error) // 4. 解析AI响应 decision, err := parseFullDecisionResponse(aiResponse, ctx.Account.TotalEquity, ctx.BTCETHLeverage, ctx.AltcoinLeverage) if err != nil { - return nil, fmt.Errorf("解析AI响应失败: %w", err) + return decision, fmt.Errorf("解析AI响应失败: %w", err) } decision.Timestamp = time.Now() @@ -427,7 +427,7 @@ func parseFullDecisionResponse(aiResponse string, accountEquity float64, btcEthL return &FullDecision{ CoTTrace: cotTrace, Decisions: []Decision{}, - }, fmt.Errorf("提取决策失败: %w\n\n=== AI思维链分析 ===\n%s", err, cotTrace) + }, fmt.Errorf("提取决策失败: %w", err) } // 3. 验证决策 @@ -435,7 +435,7 @@ func parseFullDecisionResponse(aiResponse string, accountEquity float64, btcEthL return &FullDecision{ CoTTrace: cotTrace, Decisions: decisions, - }, fmt.Errorf("决策验证失败: %w\n\n=== AI思维链分析 ===\n%s", err, cotTrace) + }, fmt.Errorf("决策验证失败: %w", err) } return &FullDecision{ From ceb2f7b435acfd50eb2e8e9cfeff91f2045f11b3 Mon Sep 17 00:00:00 2001 From: icy Date: Fri, 31 Oct 2025 03:42:01 +0800 Subject: [PATCH 03/67] =?UTF-8?q?account=20system=E3=80=81custom=20prompt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DOCKER_DEPLOY.md | 51 ++- api/server.go | 474 ++++++++++++++++++-- auth/auth.go | 121 +++++ config.json.example | 80 ---- config/database.go | 503 ++++++++++++++++++--- decision/engine.go | 34 +- go.mod | 7 +- go.sum | 6 + main.go | 30 +- manager/trader_manager.go | 280 +++++++++++- trader/auto_trader.go | 29 +- web/public/icons/aster.svg | 23 + web/public/icons/binance.svg | 1 + web/public/icons/deepseek.svg | 1 + web/public/icons/hypeliquid.svg | 3 + web/public/icons/qwen.svg | 1 + web/src/App.tsx | 293 +++++++++---- web/src/components/AITradersPage.tsx | 586 ++++++++++++++++++------- web/src/components/ComparisonChart.tsx | 344 +++++++++++++++ web/src/components/CompetitionPage.tsx | 249 +++++++++++ web/src/components/ExchangeIcons.tsx | 120 +++++ web/src/components/Header.tsx | 58 +++ web/src/components/LoginPage.tsx | 195 ++++++++ web/src/components/ModelIcons.tsx | 36 ++ web/src/components/RegisterPage.tsx | 311 +++++++++++++ web/src/contexts/AuthContext.tsx | 209 +++++++++ web/src/hooks/useSystemConfig.ts | 29 ++ web/src/i18n/translations.ts | 104 +++++ web/src/lib/api.ts | 97 +++- web/src/lib/config.ts | 27 ++ web/src/types.ts | 34 ++ web/vite.config.ts | 2 +- 32 files changed, 3873 insertions(+), 465 deletions(-) create mode 100644 auth/auth.go delete mode 100644 config.json.example create mode 100644 web/public/icons/aster.svg create mode 100644 web/public/icons/binance.svg create mode 100644 web/public/icons/deepseek.svg create mode 100644 web/public/icons/hypeliquid.svg create mode 100644 web/public/icons/qwen.svg create mode 100644 web/src/components/ComparisonChart.tsx create mode 100644 web/src/components/CompetitionPage.tsx create mode 100644 web/src/components/ExchangeIcons.tsx create mode 100644 web/src/components/Header.tsx create mode 100644 web/src/components/LoginPage.tsx create mode 100644 web/src/components/ModelIcons.tsx create mode 100644 web/src/components/RegisterPage.tsx create mode 100644 web/src/contexts/AuthContext.tsx create mode 100644 web/src/hooks/useSystemConfig.ts create mode 100644 web/src/lib/config.ts diff --git a/DOCKER_DEPLOY.md b/DOCKER_DEPLOY.md index 536ee159..b1245b4d 100644 --- a/DOCKER_DEPLOY.md +++ b/DOCKER_DEPLOY.md @@ -64,23 +64,23 @@ nano config.json # 或使用其他编辑器 **必须配置的字段:** ```json { - "traders": [ - { - "id": "my_trader", - "name": "My AI Trader", - "ai_model": "deepseek", - "binance_api_key": "YOUR_BINANCE_API_KEY", // ← 填入你的币安 API Key - "binance_secret_key": "YOUR_BINANCE_SECRET_KEY", // ← 填入你的币安 Secret Key - "deepseek_key": "YOUR_DEEPSEEK_API_KEY", // ← 填入你的 DeepSeek API Key - "initial_balance": 1000.0, - "scan_interval_minutes": 3 - } - ], "use_default_coins": true, - "api_server_port": 8080 + "api_server_port": 8081, + "jwt_secret": "YOUR_JWT_SECRET_CHANGE_IN_PRODUCTION" // ← 填入一个长随机字符串作为JWT密钥 } ``` +> **⚠️ 重要安全提醒**: +> - `jwt_secret` 字段是用户认证系统的关键安全配置 +> - **必须设置一个长度至少32位的随机字符串** +> - 在生产环境中,建议使用64位以上的随机字符串 +> - 可以使用命令生成:`openssl rand -base64 64` + +**配置说明:** +- 🔐 **用户认证**:系统现在支持用户注册登录,每个用户都有独立的AI模型和交易所配置 +- 🚫 **移除traders配置**:不再需要在config.json中预配置交易员,用户可以通过Web界面创建 +- 🔑 **JWT密钥**:用于保护用户会话安全,强烈建议在生产环境中设置复杂密钥 + ### 第 2 步:一键启动 ```bash @@ -310,23 +310,38 @@ docker system prune -a --volumes ## 🔐 安全建议 -1. **不要将 config.json 提交到 Git** +1. **JWT密钥安全配置** + ```bash + # 生成强随机JWT密钥 + openssl rand -base64 64 + + # 或者使用其他工具生成 + head -c 64 /dev/urandom | base64 + ``` + + **JWT密钥要求:** + - 长度至少32位,推荐64位以上 + - 使用随机生成的字符串 + - 在生产环境中绝不使用默认值 + - 定期更换(会使现有用户需要重新登录) + +2. **不要将 config.json 提交到 Git** ```bash # 确保 config.json 在 .gitignore 中 echo "config.json" >> .gitignore ``` -2. **使用环境变量存储敏感信息** +3. **使用环境变量存储敏感信息** ```yaml # docker-compose.yml services: backend: environment: - - BINANCE_API_KEY=${BINANCE_API_KEY} - - BINANCE_SECRET_KEY=${BINANCE_SECRET_KEY} + - JWT_SECRET=${JWT_SECRET} + # 用户的API密钥现在通过Web界面配置,不再需要环境变量 ``` -3. **限制 API 访问** +4. **限制 API 访问** ```yaml # 只允许本地访问 services: diff --git a/api/server.go b/api/server.go index c82c7bc1..1cac868f 100644 --- a/api/server.go +++ b/api/server.go @@ -4,11 +4,14 @@ import ( "fmt" "log" "net/http" + "nofx/auth" "nofx/config" "nofx/manager" + "strings" "time" "github.com/gin-gonic/gin" + "github.com/google/uuid" ) // Server HTTP API服务器 @@ -66,30 +69,51 @@ func (s *Server) setupRoutes() { // API路由组 api := s.router.Group("/api") { - // AI交易员管理 - api.GET("/traders", s.handleTraderList) - api.POST("/traders", s.handleCreateTrader) - api.DELETE("/traders/:id", s.handleDeleteTrader) - api.POST("/traders/:id/start", s.handleStartTrader) - api.POST("/traders/:id/stop", s.handleStopTrader) + // 认证相关路由(无需认证) + api.POST("/register", s.handleRegister) + api.POST("/login", s.handleLogin) + api.POST("/verify-otp", s.handleVerifyOTP) + api.POST("/complete-registration", s.handleCompleteRegistration) + + // 系统支持的模型和交易所(无需认证) + api.GET("/supported-models", s.handleGetSupportedModels) + api.GET("/supported-exchanges", s.handleGetSupportedExchanges) + + // 系统配置(无需认证) + api.GET("/config", s.handleGetSystemConfig) - // AI模型配置 - api.GET("/models", s.handleGetModelConfigs) - api.PUT("/models", s.handleUpdateModelConfigs) + // 需要认证的路由 + protected := api.Group("/", s.authMiddleware()) + { + // AI交易员管理 + protected.GET("/traders", s.handleTraderList) + protected.POST("/traders", s.handleCreateTrader) + protected.DELETE("/traders/:id", s.handleDeleteTrader) + protected.POST("/traders/:id/start", s.handleStartTrader) + protected.POST("/traders/:id/stop", s.handleStopTrader) + protected.PUT("/traders/:id/prompt", s.handleUpdateTraderPrompt) - // 交易所配置 - api.GET("/exchanges", s.handleGetExchangeConfigs) - api.PUT("/exchanges", s.handleUpdateExchangeConfigs) + // AI模型配置 + protected.GET("/models", s.handleGetModelConfigs) + protected.PUT("/models", s.handleUpdateModelConfigs) - // 指定trader的数据(使用query参数 ?trader_id=xxx) - api.GET("/status", s.handleStatus) - api.GET("/account", s.handleAccount) - api.GET("/positions", s.handlePositions) - api.GET("/decisions", s.handleDecisions) - api.GET("/decisions/latest", s.handleLatestDecisions) - api.GET("/statistics", s.handleStatistics) - api.GET("/equity-history", s.handleEquityHistory) - api.GET("/performance", s.handlePerformance) + // 交易所配置 + protected.GET("/exchanges", s.handleGetExchangeConfigs) + protected.PUT("/exchanges", s.handleUpdateExchangeConfigs) + + // 竞赛总览 + protected.GET("/competition", s.handleCompetition) + + // 指定trader的数据(使用query参数 ?trader_id=xxx) + protected.GET("/status", s.handleStatus) + protected.GET("/account", s.handleAccount) + protected.GET("/positions", s.handlePositions) + protected.GET("/decisions", s.handleDecisions) + protected.GET("/decisions/latest", s.handleLatestDecisions) + protected.GET("/statistics", s.handleStatistics) + protected.GET("/equity-history", s.handleEquityHistory) + protected.GET("/performance", s.handlePerformance) + } } } @@ -101,17 +125,40 @@ func (s *Server) handleHealth(c *gin.Context) { }) } +// handleGetSystemConfig 获取系统配置(客户端需要知道的配置) +func (s *Server) handleGetSystemConfig(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "admin_mode": auth.IsAdminMode(), + }) +} + // getTraderFromQuery 从query参数获取trader func (s *Server) getTraderFromQuery(c *gin.Context) (*manager.TraderManager, string, error) { + userID := c.GetString("user_id") traderID := c.Query("trader_id") + + // 确保用户的交易员已加载到内存中 + err := s.traderManager.LoadUserTraders(s.database, userID) + if err != nil { + log.Printf("⚠️ 加载用户 %s 的交易员失败: %v", userID, err) + } + if traderID == "" { - // 如果没有指定trader_id,返回第一个trader + // 如果没有指定trader_id,返回该用户的第一个trader ids := s.traderManager.GetTraderIDs() if len(ids) == 0 { return nil, "", fmt.Errorf("没有可用的trader") } - traderID = ids[0] + + // 获取用户的交易员列表,优先返回用户自己的交易员 + userTraders, err := s.database.GetTraders(userID) + if err == nil && len(userTraders) > 0 { + traderID = userTraders[0].ID + } else { + traderID = ids[0] + } } + return s.traderManager, traderID, nil } @@ -121,6 +168,8 @@ type CreateTraderRequest struct { AIModelID string `json:"ai_model_id" binding:"required"` ExchangeID string `json:"exchange_id" binding:"required"` InitialBalance float64 `json:"initial_balance"` + CustomPrompt string `json:"custom_prompt"` + OverrideBasePrompt bool `json:"override_base_prompt"` } type ModelConfig struct { @@ -150,15 +199,20 @@ type UpdateModelConfigRequest struct { type UpdateExchangeConfigRequest struct { Exchanges map[string]struct { - Enabled bool `json:"enabled"` - APIKey string `json:"api_key"` - SecretKey string `json:"secret_key"` - Testnet bool `json:"testnet"` + Enabled bool `json:"enabled"` + APIKey string `json:"api_key"` + SecretKey string `json:"secret_key"` + Testnet bool `json:"testnet"` + HyperliquidWalletAddr string `json:"hyperliquid_wallet_addr"` + AsterUser string `json:"aster_user"` + AsterSigner string `json:"aster_signer"` + AsterPrivateKey string `json:"aster_private_key"` } `json:"exchanges"` } // handleCreateTrader 创建新的AI交易员 func (s *Server) handleCreateTrader(c *gin.Context) { + userID := c.GetString("user_id") var req CreateTraderRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) @@ -171,10 +225,13 @@ func (s *Server) handleCreateTrader(c *gin.Context) { // 创建交易员配置 trader := &config.TraderConfig{ ID: traderID, + UserID: userID, Name: req.Name, AIModelID: req.AIModelID, ExchangeID: req.ExchangeID, InitialBalance: req.InitialBalance, + CustomPrompt: req.CustomPrompt, + OverrideBasePrompt: req.OverrideBasePrompt, ScanIntervalMinutes: 3, // 默认3分钟 IsRunning: false, } @@ -186,6 +243,13 @@ func (s *Server) handleCreateTrader(c *gin.Context) { return } + // 立即将新交易员加载到TraderManager中 + err = s.traderManager.LoadUserTraders(s.database, userID) + if err != nil { + log.Printf("⚠️ 加载用户交易员到内存失败: %v", err) + // 这里不返回错误,因为交易员已经成功创建到数据库 + } + log.Printf("✓ 创建交易员成功: %s (模型: %s, 交易所: %s)", req.Name, req.AIModelID, req.ExchangeID) c.JSON(http.StatusCreated, gin.H{ @@ -198,10 +262,11 @@ func (s *Server) handleCreateTrader(c *gin.Context) { // handleDeleteTrader 删除交易员 func (s *Server) handleDeleteTrader(c *gin.Context) { + userID := c.GetString("user_id") traderID := c.Param("id") // 从数据库删除 - err := s.database.DeleteTrader(traderID) + err := s.database.DeleteTrader(userID, traderID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("删除交易员失败: %v", err)}) return @@ -246,7 +311,8 @@ func (s *Server) handleStartTrader(c *gin.Context) { }() // 更新数据库中的运行状态 - err = s.database.UpdateTraderStatus(traderID, true) + userID := c.GetString("user_id") + err = s.database.UpdateTraderStatus(userID, traderID, true) if err != nil { log.Printf("⚠️ 更新交易员状态失败: %v", err) } @@ -276,7 +342,8 @@ func (s *Server) handleStopTrader(c *gin.Context) { trader.Stop() // 更新数据库中的运行状态 - err = s.database.UpdateTraderStatus(traderID, false) + userID := c.GetString("user_id") + err = s.database.UpdateTraderStatus(userID, traderID, false) if err != nil { log.Printf("⚠️ 更新交易员状态失败: %v", err) } @@ -285,19 +352,57 @@ func (s *Server) handleStopTrader(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "交易员已停止"}) } +// handleUpdateTraderPrompt 更新交易员自定义Prompt +func (s *Server) handleUpdateTraderPrompt(c *gin.Context) { + traderID := c.Param("id") + userID := c.GetString("user_id") + + var req struct { + CustomPrompt string `json:"custom_prompt"` + OverrideBasePrompt bool `json:"override_base_prompt"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 更新数据库 + err := s.database.UpdateTraderCustomPrompt(userID, traderID, req.CustomPrompt, req.OverrideBasePrompt) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新自定义prompt失败: %v", err)}) + return + } + + // 如果trader在内存中,更新其custom prompt和override设置 + trader, err := s.traderManager.GetTrader(traderID) + if err == nil { + trader.SetCustomPrompt(req.CustomPrompt) + trader.SetOverrideBasePrompt(req.OverrideBasePrompt) + log.Printf("✓ 已更新交易员 %s 的自定义prompt (覆盖基础=%v)", trader.GetName(), req.OverrideBasePrompt) + } + + c.JSON(http.StatusOK, gin.H{"message": "自定义prompt已更新"}) +} + // handleGetModelConfigs 获取AI模型配置 func (s *Server) handleGetModelConfigs(c *gin.Context) { - models, err := s.database.GetAIModels() + userID := c.GetString("user_id") + log.Printf("🔍 查询用户 %s 的AI模型配置", userID) + models, err := s.database.GetAIModels(userID) if err != nil { + log.Printf("❌ 获取AI模型配置失败: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("获取AI模型配置失败: %v", err)}) return } + log.Printf("✅ 找到 %d 个AI模型配置", len(models)) c.JSON(http.StatusOK, models) } // handleUpdateModelConfigs 更新AI模型配置 func (s *Server) handleUpdateModelConfigs(c *gin.Context) { + userID := c.GetString("user_id") var req UpdateModelConfigRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) @@ -306,7 +411,7 @@ func (s *Server) handleUpdateModelConfigs(c *gin.Context) { // 更新每个模型的配置 for modelID, modelData := range req.Models { - err := s.database.UpdateAIModel(modelID, modelData.Enabled, modelData.APIKey) + err := s.database.UpdateAIModel(userID, modelID, modelData.Enabled, modelData.APIKey) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新模型 %s 失败: %v", modelID, err)}) return @@ -319,17 +424,22 @@ func (s *Server) handleUpdateModelConfigs(c *gin.Context) { // handleGetExchangeConfigs 获取交易所配置 func (s *Server) handleGetExchangeConfigs(c *gin.Context) { - exchanges, err := s.database.GetExchanges() + userID := c.GetString("user_id") + log.Printf("🔍 查询用户 %s 的交易所配置", userID) + exchanges, err := s.database.GetExchanges(userID) if err != nil { + log.Printf("❌ 获取交易所配置失败: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("获取交易所配置失败: %v", err)}) return } + log.Printf("✅ 找到 %d 个交易所配置", len(exchanges)) c.JSON(http.StatusOK, exchanges) } // handleUpdateExchangeConfigs 更新交易所配置 func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) { + userID := c.GetString("user_id") var req UpdateExchangeConfigRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) @@ -338,7 +448,7 @@ func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) { // 更新每个交易所的配置 for exchangeID, exchangeData := range req.Exchanges { - err := s.database.UpdateExchange(exchangeID, exchangeData.Enabled, exchangeData.APIKey, exchangeData.SecretKey, exchangeData.Testnet) + err := s.database.UpdateExchange(userID, exchangeID, exchangeData.Enabled, exchangeData.APIKey, exchangeData.SecretKey, exchangeData.Testnet, exchangeData.HyperliquidWalletAddr, exchangeData.AsterUser, exchangeData.AsterSigner, exchangeData.AsterPrivateKey) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新交易所 %s 失败: %v", exchangeID, err)}) return @@ -351,7 +461,8 @@ func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) { // handleTraderList trader列表 func (s *Server) handleTraderList(c *gin.Context) { - traders, err := s.database.GetTraders() + userID := c.GetString("user_id") + traders, err := s.database.GetTraders(userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("获取交易员列表失败: %v", err)}) return @@ -539,6 +650,27 @@ func (s *Server) handleStatistics(c *gin.Context) { c.JSON(http.StatusOK, stats) } +// handleCompetition 竞赛总览(对比所有trader) +func (s *Server) handleCompetition(c *gin.Context) { + userID := c.GetString("user_id") + + // 确保用户的交易员已加载到内存中 + err := s.traderManager.LoadUserTraders(s.database, userID) + if err != nil { + log.Printf("⚠️ 加载用户 %s 的交易员失败: %v", userID, err) + } + + competition, err := s.traderManager.GetCompetitionData(userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": fmt.Sprintf("获取竞赛数据失败: %v", err), + }) + return + } + + c.JSON(http.StatusOK, competition) +} + // handleEquityHistory 收益率历史数据 func (s *Server) handleEquityHistory(c *gin.Context) { _, traderID, err := s.getTraderFromQuery(c) @@ -652,6 +784,278 @@ func (s *Server) handlePerformance(c *gin.Context) { c.JSON(http.StatusOK, performance) } +// authMiddleware JWT认证中间件 +func (s *Server) authMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // 如果是管理员模式,直接使用admin用户 + if auth.IsAdminMode() { + c.Set("user_id", "admin") + c.Set("email", "admin@localhost") + c.Next() + return + } + + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "缺少Authorization头"}) + c.Abort() + return + } + + // 检查Bearer token格式 + tokenParts := strings.Split(authHeader, " ") + if len(tokenParts) != 2 || tokenParts[0] != "Bearer" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的Authorization格式"}) + c.Abort() + return + } + + // 验证JWT token + claims, err := auth.ValidateJWT(tokenParts[1]) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的token: " + err.Error()}) + c.Abort() + return + } + + // 将用户信息存储到上下文中 + c.Set("user_id", claims.UserID) + c.Set("email", claims.Email) + c.Next() + } +} + +// handleRegister 处理用户注册请求 +func (s *Server) handleRegister(c *gin.Context) { + var req struct { + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required,min=6"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 检查邮箱是否已存在 + _, err := s.database.GetUserByEmail(req.Email) + if err == nil { + c.JSON(http.StatusConflict, gin.H{"error": "邮箱已被注册"}) + return + } + + // 生成密码哈希 + passwordHash, err := auth.HashPassword(req.Password) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "密码处理失败"}) + return + } + + // 生成OTP密钥 + otpSecret, err := auth.GenerateOTPSecret() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "OTP密钥生成失败"}) + return + } + + // 创建用户(未验证OTP状态) + userID := uuid.New().String() + user := &config.User{ + ID: userID, + Email: req.Email, + PasswordHash: passwordHash, + OTPSecret: otpSecret, + OTPVerified: false, + } + + err = s.database.CreateUser(user) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "创建用户失败: " + err.Error()}) + return + } + + // 返回OTP设置信息 + qrCodeURL := auth.GetOTPQRCodeURL(otpSecret, req.Email) + c.JSON(http.StatusOK, gin.H{ + "user_id": userID, + "email": req.Email, + "otp_secret": otpSecret, + "qr_code_url": qrCodeURL, + "message": "请使用Google Authenticator扫描二维码并验证OTP", + }) +} + +// handleCompleteRegistration 完成注册(验证OTP) +func (s *Server) handleCompleteRegistration(c *gin.Context) { + var req struct { + UserID string `json:"user_id" binding:"required"` + OTPCode string `json:"otp_code" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 获取用户信息 + user, err := s.database.GetUserByID(req.UserID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) + return + } + + // 验证OTP + if !auth.VerifyOTP(user.OTPSecret, req.OTPCode) { + c.JSON(http.StatusBadRequest, gin.H{"error": "OTP验证码错误"}) + return + } + + // 更新用户OTP验证状态 + err = s.database.UpdateUserOTPVerified(req.UserID, true) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "更新用户状态失败"}) + return + } + + // 生成JWT token + token, err := auth.GenerateJWT(user.ID, user.Email) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "生成token失败"}) + return + } + + // 初始化用户的默认模型和交易所配置 + err = s.initUserDefaultConfigs(user.ID) + if err != nil { + log.Printf("初始化用户默认配置失败: %v", err) + } + + c.JSON(http.StatusOK, gin.H{ + "token": token, + "user_id": user.ID, + "email": user.Email, + "message": "注册完成", + }) +} + +// handleLogin 处理用户登录请求 +func (s *Server) handleLogin(c *gin.Context) { + var req struct { + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 获取用户信息 + user, err := s.database.GetUserByEmail(req.Email) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "邮箱或密码错误"}) + return + } + + // 验证密码 + if !auth.CheckPassword(req.Password, user.PasswordHash) { + c.JSON(http.StatusUnauthorized, gin.H{"error": "邮箱或密码错误"}) + return + } + + // 检查OTP是否已验证 + if !user.OTPVerified { + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "账户未完成OTP设置", + "user_id": user.ID, + "requires_otp_setup": true, + }) + return + } + + // 返回需要OTP验证的状态 + c.JSON(http.StatusOK, gin.H{ + "user_id": user.ID, + "email": user.Email, + "message": "请输入Google Authenticator验证码", + "requires_otp": true, + }) +} + +// handleVerifyOTP 验证OTP并完成登录 +func (s *Server) handleVerifyOTP(c *gin.Context) { + var req struct { + UserID string `json:"user_id" binding:"required"` + OTPCode string `json:"otp_code" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 获取用户信息 + user, err := s.database.GetUserByID(req.UserID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) + return + } + + // 验证OTP + if !auth.VerifyOTP(user.OTPSecret, req.OTPCode) { + c.JSON(http.StatusBadRequest, gin.H{"error": "验证码错误"}) + return + } + + // 生成JWT token + token, err := auth.GenerateJWT(user.ID, user.Email) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "生成token失败"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "token": token, + "user_id": user.ID, + "email": user.Email, + "message": "登录成功", + }) +} + +// initUserDefaultConfigs 为新用户初始化默认的模型和交易所配置 +func (s *Server) initUserDefaultConfigs(userID string) error { + // 注释掉自动创建默认配置,让用户手动添加 + // 这样新用户注册后不会自动有配置项 + log.Printf("用户 %s 注册完成,等待手动配置AI模型和交易所", userID) + return nil +} + +// handleGetSupportedModels 获取系统支持的AI模型列表 +func (s *Server) handleGetSupportedModels(c *gin.Context) { + // 返回系统支持的AI模型(从default用户获取) + models, err := s.database.GetAIModels("default") + if err != nil { + log.Printf("❌ 获取支持的AI模型失败: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取支持的AI模型失败"}) + return + } + + c.JSON(http.StatusOK, models) +} + +// handleGetSupportedExchanges 获取系统支持的交易所列表 +func (s *Server) handleGetSupportedExchanges(c *gin.Context) { + // 返回系统支持的交易所(从default用户获取) + exchanges, err := s.database.GetExchanges("default") + if err != nil { + log.Printf("❌ 获取支持的交易所失败: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取支持的交易所失败"}) + return + } + + c.JSON(http.StatusOK, exchanges) +} + // Start 启动服务器 func (s *Server) Start() error { addr := fmt.Sprintf(":%d", s.port) diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 00000000..685d08e6 --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,121 @@ +package auth + +import ( + "crypto/rand" + "fmt" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/pquerna/otp/totp" + "golang.org/x/crypto/bcrypt" +) + +// JWTSecret JWT密钥,将从配置中动态设置 +var JWTSecret []byte + +// AdminMode 管理员模式标志 +var AdminMode bool = false + +// OTPIssuer OTP发行者名称 +const OTPIssuer = "nofxAI" + +// SetJWTSecret 设置JWT密钥 +func SetJWTSecret(secret string) { + JWTSecret = []byte(secret) +} + +// SetAdminMode 设置管理员模式 +func SetAdminMode(enabled bool) { + AdminMode = enabled +} + +// IsAdminMode 检查是否为管理员模式 +func IsAdminMode() bool { + return AdminMode +} + +// Claims JWT声明 +type Claims struct { + UserID string `json:"user_id"` + Email string `json:"email"` + jwt.RegisteredClaims +} + +// HashPassword 哈希密码 +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes), err +} + +// CheckPassword 验证密码 +func CheckPassword(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +// GenerateOTPSecret 生成OTP密钥 +func GenerateOTPSecret() (string, error) { + secret := make([]byte, 20) + _, err := rand.Read(secret) + if err != nil { + return "", err + } + + key, err := totp.Generate(totp.GenerateOpts{ + Issuer: OTPIssuer, + AccountName: uuid.New().String(), + }) + if err != nil { + return "", err + } + + return key.Secret(), nil +} + +// VerifyOTP 验证OTP码 +func VerifyOTP(secret, code string) bool { + return totp.Validate(code, secret) +} + +// GenerateJWT 生成JWT token +func GenerateJWT(userID, email string) (string, error) { + claims := Claims{ + UserID: userID, + Email: email, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), // 24小时过期 + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + Issuer: "nofxAI", + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(JWTSecret) +} + +// ValidateJWT 验证JWT token +func ValidateJWT(tokenString string) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("意外的签名方法: %v", token.Header["alg"]) + } + return JWTSecret, nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + + return nil, fmt.Errorf("无效的token") +} + +// GetOTPQRCodeURL 获取OTP二维码URL +func GetOTPQRCodeURL(secret, email string) string { + return fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s", OTPIssuer, email, secret, OTPIssuer) +} \ No newline at end of file diff --git a/config.json.example b/config.json.example deleted file mode 100644 index 8f41523e..00000000 --- a/config.json.example +++ /dev/null @@ -1,80 +0,0 @@ -{ - "traders": [ - { - "id": "hyperliquid_deepseek", - "name": "Hyperliquid DeepSeek Trader", - "ai_model": "deepseek", - "exchange": "hyperliquid", - "hyperliquid_private_key": "your_ethereum_private_key_without_0x_prefix", - "hyperliquid_wallet_addr": "your_ethereum_address", - "hyperliquid_testnet": false, - "deepseek_key": "your_deepseek_api_key", - "initial_balance": 1000, - "scan_interval_minutes": 3 - }, - { - "id": "binance_qwen", - "name": "Binance Qwen Trader", - "ai_model": "qwen", - "exchange": "binance", - "binance_api_key": "your_binance_api_key", - "binance_secret_key": "your_binance_secret_key", - "qwen_key": "your_qwen_api_key", - "initial_balance": 1000, - "scan_interval_minutes": 3 - }, - { - "id": "binance_custom", - "name": "Binance Custom API Trader", - "ai_model": "custom", - "exchange": "binance", - "binance_api_key": "your_binance_api_key", - "binance_secret_key": "your_binance_secret_key", - "custom_api_url": "https://api.openai.com/v1", - "custom_api_key": "sk-your-api-key", - "custom_model_name": "gpt-4o", - "initial_balance": 1000, - "scan_interval_minutes": 3 - }, - { - "id": "aster_deepseek", - "name": "Aster DeepSeek Trader", - "ai_model": "deepseek", - "exchange": "aster", - - // 注意请仔细阅读这三个提示 请进入https://www.asterdex.com/en/api-wallet网站 -> 选择专业api -> 创建新api获取以下信息 - // user: 主钱包地址 (登录地址/连接到aster的钱包地址) - // signer: API钱包地址 (点击生成地址后生成的地址) - // privateKey: API钱包私钥 (生成地址对应的私钥) - - "aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e", - "aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0", - "aster_private_key": "your_aster_api_wallet_private_key_without_0x_prefix", - - "deepseek_key": "your_deepseek_api_key", - "initial_balance": 1000.0, - "scan_interval_minutes": 3 - } - ], - "leverage": { - "btc_eth_leverage": 5, - "altcoin_leverage": 5 - }, - "use_default_coins": true, - "default_coins": [ - "BTCUSDT", - "ETHUSDT", - "SOLUSDT", - "BNBUSDT", - "XRPUSDT", - "DOGEUSDT", - "ADAUSDT", - "HYPEUSDT", - ], - "coin_pool_api_url": "", - "oi_top_api_url": "", - "api_server_port": 8080, - "max_daily_loss": 10.0, - "max_drawdown": 20.0, - "stop_trading_minutes": 60 -} diff --git a/config/database.go b/config/database.go index 8e1c193a..dd40866c 100644 --- a/config/database.go +++ b/config/database.go @@ -1,8 +1,11 @@ package config import ( + "crypto/rand" "database/sql" + "encoding/base32" "fmt" + "log" "time" _ "github.com/mattn/go-sqlite3" @@ -38,30 +41,41 @@ func (d *Database) createTables() error { // AI模型配置表 `CREATE TABLE IF NOT EXISTS ai_models ( id TEXT PRIMARY KEY, + user_id TEXT NOT NULL DEFAULT 'default', name TEXT NOT NULL, provider TEXT NOT NULL, enabled BOOLEAN DEFAULT 0, api_key TEXT DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`, // 交易所配置表 `CREATE TABLE IF NOT EXISTS exchanges ( id TEXT PRIMARY KEY, + user_id TEXT NOT NULL DEFAULT 'default', name TEXT NOT NULL, type TEXT NOT NULL, -- 'cex' or 'dex' enabled BOOLEAN DEFAULT 0, api_key TEXT DEFAULT '', secret_key TEXT DEFAULT '', testnet BOOLEAN DEFAULT 0, + -- Hyperliquid 特定字段 + hyperliquid_wallet_addr TEXT DEFAULT '', + -- Aster 特定字段 + aster_user TEXT DEFAULT '', + aster_signer TEXT DEFAULT '', + aster_private_key TEXT DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`, // 交易员配置表 `CREATE TABLE IF NOT EXISTS traders ( id TEXT PRIMARY KEY, + user_id TEXT NOT NULL DEFAULT 'default', name TEXT NOT NULL, ai_model_id TEXT NOT NULL, exchange_id TEXT NOT NULL, @@ -70,10 +84,22 @@ func (d *Database) createTables() error { is_running BOOLEAN DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (ai_model_id) REFERENCES ai_models(id), FOREIGN KEY (exchange_id) REFERENCES exchanges(id) )`, + // 用户表 + `CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + otp_secret TEXT, + otp_verified BOOLEAN DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`, + // 系统配置表 `CREATE TABLE IF NOT EXISTS system_config ( key TEXT PRIMARY KEY, @@ -82,6 +108,12 @@ func (d *Database) createTables() error { )`, // 触发器:自动更新 updated_at + `CREATE TRIGGER IF NOT EXISTS update_users_updated_at + AFTER UPDATE ON users + BEGIN + UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; + END`, + `CREATE TRIGGER IF NOT EXISTS update_ai_models_updated_at AFTER UPDATE ON ai_models BEGIN @@ -113,12 +145,33 @@ func (d *Database) createTables() error { } } + // 为现有数据库添加新字段(向后兼容) + alterQueries := []string{ + `ALTER TABLE exchanges ADD COLUMN hyperliquid_wallet_addr TEXT DEFAULT ''`, + `ALTER TABLE exchanges ADD COLUMN aster_user TEXT DEFAULT ''`, + `ALTER TABLE exchanges ADD COLUMN aster_signer TEXT DEFAULT ''`, + `ALTER TABLE exchanges ADD COLUMN aster_private_key TEXT DEFAULT ''`, + `ALTER TABLE traders ADD COLUMN custom_prompt TEXT DEFAULT ''`, + `ALTER TABLE traders ADD COLUMN override_base_prompt BOOLEAN DEFAULT 0`, + } + + for _, query := range alterQueries { + // 忽略已存在字段的错误 + d.db.Exec(query) + } + + // 检查是否需要迁移exchanges表的主键结构 + err := d.migrateExchangesTable() + if err != nil { + log.Printf("⚠️ 迁移exchanges表失败: %v", err) + } + return nil } // initDefaultData 初始化默认数据 func (d *Database) initDefaultData() error { - // 初始化AI模型 + // 初始化AI模型(使用default用户) aiModels := []struct { id, name, provider string }{ @@ -128,26 +181,27 @@ func (d *Database) initDefaultData() error { for _, model := range aiModels { _, err := d.db.Exec(` - INSERT OR IGNORE INTO ai_models (id, name, provider, enabled) - VALUES (?, ?, ?, 0) + INSERT OR IGNORE INTO ai_models (id, user_id, name, provider, enabled) + VALUES (?, 'default', ?, ?, 0) `, model.id, model.name, model.provider) if err != nil { return fmt.Errorf("初始化AI模型失败: %w", err) } } - // 初始化交易所 + // 初始化交易所(使用default用户) exchanges := []struct { id, name, typ string }{ - {"binance", "Binance", "cex"}, - {"hyperliquid", "Hyperliquid", "dex"}, + {"binance", "Binance Futures", "binance"}, + {"hyperliquid", "Hyperliquid", "hyperliquid"}, + {"aster", "Aster DEX", "aster"}, } for _, exchange := range exchanges { _, err := d.db.Exec(` - INSERT OR IGNORE INTO exchanges (id, name, type, enabled) - VALUES (?, ?, ?, 0) + INSERT OR IGNORE INTO exchanges (id, user_id, name, type, enabled) + VALUES (?, 'default', ?, ?, 0) `, exchange.id, exchange.name, exchange.typ) if err != nil { return fmt.Errorf("初始化交易所失败: %w", err) @@ -156,7 +210,7 @@ func (d *Database) initDefaultData() error { // 初始化系统配置 systemConfigs := map[string]string{ - "api_server_port": "8081", + "api_server_port": "8080", "use_default_coins": "true", "coin_pool_api_url": "", "oi_top_api_url": "", @@ -178,9 +232,103 @@ func (d *Database) initDefaultData() error { return nil } +// migrateExchangesTable 迁移exchanges表支持多用户 +func (d *Database) migrateExchangesTable() error { + // 检查是否已经迁移过 + var count int + err := d.db.QueryRow(` + SELECT COUNT(*) FROM sqlite_master + WHERE type='table' AND name='exchanges_new' + `).Scan(&count) + if err != nil { + return err + } + + // 如果已经迁移过,直接返回 + if count > 0 { + return nil + } + + log.Printf("🔄 开始迁移exchanges表...") + + // 创建新的exchanges表,使用复合主键 + _, err = d.db.Exec(` + CREATE TABLE exchanges_new ( + id TEXT NOT NULL, + user_id TEXT NOT NULL DEFAULT 'default', + name TEXT NOT NULL, + type TEXT NOT NULL, + enabled BOOLEAN DEFAULT 0, + api_key TEXT DEFAULT '', + secret_key TEXT DEFAULT '', + testnet BOOLEAN DEFAULT 0, + hyperliquid_wallet_addr TEXT DEFAULT '', + aster_user TEXT DEFAULT '', + aster_signer TEXT DEFAULT '', + aster_private_key TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id, user_id), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ) + `) + if err != nil { + return fmt.Errorf("创建新exchanges表失败: %w", err) + } + + // 复制数据到新表 + _, err = d.db.Exec(` + INSERT INTO exchanges_new + SELECT * FROM exchanges + `) + if err != nil { + return fmt.Errorf("复制数据失败: %w", err) + } + + // 删除旧表 + _, err = d.db.Exec(`DROP TABLE exchanges`) + if err != nil { + return fmt.Errorf("删除旧表失败: %w", err) + } + + // 重命名新表 + _, err = d.db.Exec(`ALTER TABLE exchanges_new RENAME TO exchanges`) + if err != nil { + return fmt.Errorf("重命名表失败: %w", err) + } + + // 重新创建触发器 + _, err = d.db.Exec(` + CREATE TRIGGER IF NOT EXISTS update_exchanges_updated_at + AFTER UPDATE ON exchanges + BEGIN + UPDATE exchanges SET updated_at = CURRENT_TIMESTAMP + WHERE id = NEW.id AND user_id = NEW.user_id; + END + `) + if err != nil { + return fmt.Errorf("创建触发器失败: %w", err) + } + + log.Printf("✅ exchanges表迁移完成") + return nil +} + +// User 用户配置 +type User struct { + ID string `json:"id"` + Email string `json:"email"` + PasswordHash string `json:"-"` // 不返回到前端 + OTPSecret string `json:"-"` // 不返回到前端 + OTPVerified bool `json:"otp_verified"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + // AIModelConfig AI模型配置 type AIModelConfig struct { ID string `json:"id"` + UserID string `json:"user_id"` Name string `json:"name"` Provider string `json:"provider"` Enabled bool `json:"enabled"` @@ -192,45 +340,139 @@ type AIModelConfig struct { // ExchangeConfig 交易所配置 type ExchangeConfig struct { ID string `json:"id"` + UserID string `json:"user_id"` Name string `json:"name"` Type string `json:"type"` Enabled bool `json:"enabled"` APIKey string `json:"apiKey"` SecretKey string `json:"secretKey"` Testnet bool `json:"testnet"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + // Hyperliquid 特定字段 + HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"` + // Aster 特定字段 + AsterUser string `json:"asterUser"` + AsterSigner string `json:"asterSigner"` + AsterPrivateKey string `json:"asterPrivateKey"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // TraderConfig 交易员配置 type TraderConfig struct { ID string `json:"id"` + UserID string `json:"user_id"` Name string `json:"name"` AIModelID string `json:"ai_model_id"` ExchangeID string `json:"exchange_id"` InitialBalance float64 `json:"initial_balance"` ScanIntervalMinutes int `json:"scan_interval_minutes"` IsRunning bool `json:"is_running"` + CustomPrompt string `json:"custom_prompt"` // 自定义交易策略prompt + OverrideBasePrompt bool `json:"override_base_prompt"` // 是否覆盖基础prompt CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } -// GetAIModels 获取所有AI模型配置 -func (d *Database) GetAIModels() ([]*AIModelConfig, error) { +// GenerateOTPSecret 生成OTP密钥 +func GenerateOTPSecret() (string, error) { + secret := make([]byte, 20) + _, err := rand.Read(secret) + if err != nil { + return "", err + } + return base32.StdEncoding.EncodeToString(secret), nil +} + +// CreateUser 创建用户 +func (d *Database) CreateUser(user *User) error { + _, err := d.db.Exec(` + INSERT INTO users (id, email, password_hash, otp_secret, otp_verified) + VALUES (?, ?, ?, ?, ?) + `, user.ID, user.Email, user.PasswordHash, user.OTPSecret, user.OTPVerified) + return err +} + +// EnsureAdminUser 确保admin用户存在(用于管理员模式) +func (d *Database) EnsureAdminUser() error { + // 检查admin用户是否已存在 + var count int + err := d.db.QueryRow(`SELECT COUNT(*) FROM users WHERE id = 'admin'`).Scan(&count) + if err != nil { + return err + } + + // 如果已存在,直接返回 + if count > 0 { + return nil + } + + // 创建admin用户(密码为空,因为管理员模式下不需要密码) + adminUser := &User{ + ID: "admin", + Email: "admin@localhost", + PasswordHash: "", // 管理员模式下不使用密码 + OTPSecret: "", + OTPVerified: true, + } + + return d.CreateUser(adminUser) +} + +// GetUserByEmail 通过邮箱获取用户 +func (d *Database) GetUserByEmail(email string) (*User, error) { + var user User + err := d.db.QueryRow(` + SELECT id, email, password_hash, otp_secret, otp_verified, created_at, updated_at + FROM users WHERE email = ? + `, email).Scan( + &user.ID, &user.Email, &user.PasswordHash, &user.OTPSecret, + &user.OTPVerified, &user.CreatedAt, &user.UpdatedAt, + ) + if err != nil { + return nil, err + } + return &user, nil +} + +// GetUserByID 通过ID获取用户 +func (d *Database) GetUserByID(userID string) (*User, error) { + var user User + err := d.db.QueryRow(` + SELECT id, email, password_hash, otp_secret, otp_verified, created_at, updated_at + FROM users WHERE id = ? + `, userID).Scan( + &user.ID, &user.Email, &user.PasswordHash, &user.OTPSecret, + &user.OTPVerified, &user.CreatedAt, &user.UpdatedAt, + ) + if err != nil { + return nil, err + } + return &user, nil +} + +// UpdateUserOTPVerified 更新用户OTP验证状态 +func (d *Database) UpdateUserOTPVerified(userID string, verified bool) error { + _, err := d.db.Exec(`UPDATE users SET otp_verified = ? WHERE id = ?`, verified, userID) + return err +} + +// GetAIModels 获取用户的AI模型配置 +func (d *Database) GetAIModels(userID string) ([]*AIModelConfig, error) { rows, err := d.db.Query(` - SELECT id, name, provider, enabled, api_key, created_at, updated_at - FROM ai_models ORDER BY id - `) + SELECT id, user_id, name, provider, enabled, api_key, created_at, updated_at + FROM ai_models WHERE user_id = ? ORDER BY id + `, userID) if err != nil { return nil, err } defer rows.Close() - var models []*AIModelConfig + // 初始化为空切片而不是nil,确保JSON序列化为[]而不是null + models := make([]*AIModelConfig, 0) for rows.Next() { var model AIModelConfig err := rows.Scan( - &model.ID, &model.Name, &model.Provider, + &model.ID, &model.UserID, &model.Name, &model.Provider, &model.Enabled, &model.APIKey, &model.CreatedAt, &model.UpdatedAt, ) @@ -243,31 +485,80 @@ func (d *Database) GetAIModels() ([]*AIModelConfig, error) { return models, nil } -// UpdateAIModel 更新AI模型配置 -func (d *Database) UpdateAIModel(id string, enabled bool, apiKey string) error { - _, err := d.db.Exec(` - UPDATE ai_models SET enabled = ?, api_key = ? WHERE id = ? - `, enabled, apiKey, id) - return err +// UpdateAIModel 更新AI模型配置,如果不存在则创建用户特定配置 +func (d *Database) UpdateAIModel(userID, id string, enabled bool, apiKey string) error { + // 首先尝试更新现有的用户配置 + result, err := d.db.Exec(` + UPDATE ai_models SET enabled = ?, api_key = ? WHERE id = ? AND user_id = ? + `, enabled, apiKey, id, userID) + if err != nil { + return err + } + + // 检查是否有行被更新 + rowsAffected, err := result.RowsAffected() + if err != nil { + return err + } + + // 如果没有行被更新,说明用户没有这个模型的配置,需要创建 + if rowsAffected == 0 { + // 获取模型的基本信息 + var name, provider string + err = d.db.QueryRow(` + SELECT name, provider FROM ai_models WHERE provider = ? LIMIT 1 + `, id).Scan(&name, &provider) + if err != nil { + // 如果找不到基本信息,使用默认值 + if id == "deepseek" { + name = "DeepSeek AI" + provider = "deepseek" + } else if id == "qwen" { + name = "Qwen AI" + provider = "qwen" + } else { + name = id + " AI" + provider = id + } + } + + // 创建用户特定的配置 + userModelID := fmt.Sprintf("%s_%s", userID, id) + _, err = d.db.Exec(` + INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now')) + `, userModelID, userID, name, provider, enabled, apiKey) + return err + } + + return nil } -// GetExchanges 获取所有交易所配置 -func (d *Database) GetExchanges() ([]*ExchangeConfig, error) { +// GetExchanges 获取用户的交易所配置 +func (d *Database) GetExchanges(userID string) ([]*ExchangeConfig, error) { rows, err := d.db.Query(` - SELECT id, name, type, enabled, api_key, secret_key, testnet, created_at, updated_at - FROM exchanges ORDER BY id - `) + SELECT id, user_id, name, type, enabled, api_key, secret_key, testnet, + COALESCE(hyperliquid_wallet_addr, '') as hyperliquid_wallet_addr, + COALESCE(aster_user, '') as aster_user, + COALESCE(aster_signer, '') as aster_signer, + COALESCE(aster_private_key, '') as aster_private_key, + created_at, updated_at + FROM exchanges WHERE user_id = ? ORDER BY id + `, userID) if err != nil { return nil, err } defer rows.Close() - var exchanges []*ExchangeConfig + // 初始化为空切片而不是nil,确保JSON序列化为[]而不是null + exchanges := make([]*ExchangeConfig, 0) for rows.Next() { var exchange ExchangeConfig err := rows.Scan( - &exchange.ID, &exchange.Name, &exchange.Type, + &exchange.ID, &exchange.UserID, &exchange.Name, &exchange.Type, &exchange.Enabled, &exchange.APIKey, &exchange.SecretKey, &exchange.Testnet, + &exchange.HyperliquidWalletAddr, &exchange.AsterUser, + &exchange.AsterSigner, &exchange.AsterPrivateKey, &exchange.CreatedAt, &exchange.UpdatedAt, ) if err != nil { @@ -279,29 +570,105 @@ func (d *Database) GetExchanges() ([]*ExchangeConfig, error) { return exchanges, nil } -// UpdateExchange 更新交易所配置 -func (d *Database) UpdateExchange(id string, enabled bool, apiKey, secretKey string, testnet bool) error { +// UpdateExchange 更新交易所配置,如果不存在则创建用户特定配置 +func (d *Database) UpdateExchange(userID, id string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error { + log.Printf("🔧 UpdateExchange: userID=%s, id=%s, enabled=%v", userID, id, enabled) + + // 首先尝试更新现有的用户配置 + result, err := d.db.Exec(` + UPDATE exchanges SET enabled = ?, api_key = ?, secret_key = ?, testnet = ?, + hyperliquid_wallet_addr = ?, aster_user = ?, aster_signer = ?, aster_private_key = ?, updated_at = datetime('now') + WHERE id = ? AND user_id = ? + `, enabled, apiKey, secretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, id, userID) + if err != nil { + log.Printf("❌ UpdateExchange: 更新失败: %v", err) + return err + } + + // 检查是否有行被更新 + rowsAffected, err := result.RowsAffected() + if err != nil { + log.Printf("❌ UpdateExchange: 获取影响行数失败: %v", err) + return err + } + + log.Printf("📊 UpdateExchange: 影响行数 = %d", rowsAffected) + + // 如果没有行被更新,说明用户没有这个交易所的配置,需要创建 + if rowsAffected == 0 { + log.Printf("💡 UpdateExchange: 没有现有记录,创建新记录") + + // 根据交易所ID确定基本信息 + var name, typ string + if id == "binance" { + name = "Binance Futures" + typ = "cex" + } else if id == "hyperliquid" { + name = "Hyperliquid" + typ = "dex" + } else if id == "aster" { + name = "Aster DEX" + typ = "dex" + } else { + name = id + " Exchange" + typ = "cex" + } + + log.Printf("🆕 UpdateExchange: 创建新记录 ID=%s, name=%s, type=%s", id, name, typ) + + // 创建用户特定的配置,使用原始的交易所ID + _, err = d.db.Exec(` + INSERT INTO exchanges (id, user_id, name, type, enabled, api_key, secret_key, testnet, + hyperliquid_wallet_addr, aster_user, aster_signer, aster_private_key, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now')) + `, id, userID, name, typ, enabled, apiKey, secretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey) + + if err != nil { + log.Printf("❌ UpdateExchange: 创建记录失败: %v", err) + } else { + log.Printf("✅ UpdateExchange: 创建记录成功") + } + return err + } + + log.Printf("✅ UpdateExchange: 更新现有记录成功") + return nil +} + +// CreateAIModel 创建AI模型配置 +func (d *Database) CreateAIModel(userID, id, name, provider string, enabled bool, apiKey string) error { _, err := d.db.Exec(` - UPDATE exchanges SET enabled = ?, api_key = ?, secret_key = ?, testnet = ? WHERE id = ? - `, enabled, apiKey, secretKey, testnet, id) + INSERT OR IGNORE INTO ai_models (id, user_id, name, provider, enabled, api_key) + VALUES (?, ?, ?, ?, ?, ?) + `, id, userID, name, provider, enabled, apiKey) + return err +} + +// CreateExchange 创建交易所配置 +func (d *Database) CreateExchange(userID, id, name, typ string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error { + _, err := d.db.Exec(` + INSERT OR IGNORE INTO exchanges (id, user_id, name, type, enabled, api_key, secret_key, testnet, hyperliquid_wallet_addr, aster_user, aster_signer, aster_private_key) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, id, userID, name, typ, enabled, apiKey, secretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey) return err } // CreateTrader 创建交易员 func (d *Database) CreateTrader(trader *TraderConfig) error { _, err := d.db.Exec(` - INSERT INTO traders (id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running) - VALUES (?, ?, ?, ?, ?, ?, ?) - `, trader.ID, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance, trader.ScanIntervalMinutes, trader.IsRunning) + INSERT INTO traders (id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running, custom_prompt, override_base_prompt) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, trader.ID, trader.UserID, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance, trader.ScanIntervalMinutes, trader.IsRunning, trader.CustomPrompt, trader.OverrideBasePrompt) return err } -// GetTraders 获取所有交易员 -func (d *Database) GetTraders() ([]*TraderConfig, error) { +// GetTraders 获取用户的交易员 +func (d *Database) GetTraders(userID string) ([]*TraderConfig, error) { rows, err := d.db.Query(` - SELECT id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running, created_at, updated_at - FROM traders ORDER BY created_at DESC - `) + SELECT id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running, + COALESCE(custom_prompt, '') as custom_prompt, COALESCE(override_base_prompt, 0) as override_base_prompt, created_at, updated_at + FROM traders WHERE user_id = ? ORDER BY created_at DESC + `, userID) if err != nil { return nil, err } @@ -311,9 +678,9 @@ func (d *Database) GetTraders() ([]*TraderConfig, error) { for rows.Next() { var trader TraderConfig err := rows.Scan( - &trader.ID, &trader.Name, &trader.AIModelID, &trader.ExchangeID, + &trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID, &trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning, - &trader.CreatedAt, &trader.UpdatedAt, + &trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.CreatedAt, &trader.UpdatedAt, ) if err != nil { return nil, err @@ -325,40 +692,52 @@ func (d *Database) GetTraders() ([]*TraderConfig, error) { } // UpdateTraderStatus 更新交易员状态 -func (d *Database) UpdateTraderStatus(id string, isRunning bool) error { - _, err := d.db.Exec(`UPDATE traders SET is_running = ? WHERE id = ?`, isRunning, id) +func (d *Database) UpdateTraderStatus(userID, id string, isRunning bool) error { + _, err := d.db.Exec(`UPDATE traders SET is_running = ? WHERE id = ? AND user_id = ?`, isRunning, id, userID) + return err +} + +// UpdateTraderCustomPrompt 更新交易员自定义Prompt +func (d *Database) UpdateTraderCustomPrompt(userID, id string, customPrompt string, overrideBase bool) error { + _, err := d.db.Exec(`UPDATE traders SET custom_prompt = ?, override_base_prompt = ? WHERE id = ? AND user_id = ?`, customPrompt, overrideBase, id, userID) return err } // DeleteTrader 删除交易员 -func (d *Database) DeleteTrader(id string) error { - _, err := d.db.Exec(`DELETE FROM traders WHERE id = ?`, id) +func (d *Database) DeleteTrader(userID, id string) error { + _, err := d.db.Exec(`DELETE FROM traders WHERE id = ? AND user_id = ?`, id, userID) return err } // GetTraderConfig 获取交易员完整配置(包含AI模型和交易所信息) -func (d *Database) GetTraderConfig(traderID string) (*TraderConfig, *AIModelConfig, *ExchangeConfig, error) { +func (d *Database) GetTraderConfig(userID, traderID string) (*TraderConfig, *AIModelConfig, *ExchangeConfig, error) { var trader TraderConfig var aiModel AIModelConfig var exchange ExchangeConfig err := d.db.QueryRow(` SELECT - t.id, t.name, t.ai_model_id, t.exchange_id, t.initial_balance, t.scan_interval_minutes, t.is_running, t.created_at, t.updated_at, - a.id, a.name, a.provider, a.enabled, a.api_key, a.created_at, a.updated_at, - e.id, e.name, e.type, e.enabled, e.api_key, e.secret_key, e.testnet, e.created_at, e.updated_at + t.id, t.user_id, t.name, t.ai_model_id, t.exchange_id, t.initial_balance, t.scan_interval_minutes, t.is_running, t.created_at, t.updated_at, + a.id, a.user_id, a.name, a.provider, a.enabled, a.api_key, a.created_at, a.updated_at, + e.id, e.user_id, e.name, e.type, e.enabled, e.api_key, e.secret_key, e.testnet, + COALESCE(e.hyperliquid_wallet_addr, '') as hyperliquid_wallet_addr, + COALESCE(e.aster_user, '') as aster_user, + COALESCE(e.aster_signer, '') as aster_signer, + COALESCE(e.aster_private_key, '') as aster_private_key, + e.created_at, e.updated_at FROM traders t - JOIN ai_models a ON t.ai_model_id = a.id - JOIN exchanges e ON t.exchange_id = e.id - WHERE t.id = ? - `, traderID).Scan( - &trader.ID, &trader.Name, &trader.AIModelID, &trader.ExchangeID, + JOIN ai_models a ON t.ai_model_id = a.id AND t.user_id = a.user_id + JOIN exchanges e ON t.exchange_id = e.id AND t.user_id = e.user_id + WHERE t.id = ? AND t.user_id = ? + `, traderID, userID).Scan( + &trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID, &trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning, &trader.CreatedAt, &trader.UpdatedAt, - &aiModel.ID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey, + &aiModel.ID, &aiModel.UserID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey, &aiModel.CreatedAt, &aiModel.UpdatedAt, - &exchange.ID, &exchange.Name, &exchange.Type, &exchange.Enabled, + &exchange.ID, &exchange.UserID, &exchange.Name, &exchange.Type, &exchange.Enabled, &exchange.APIKey, &exchange.SecretKey, &exchange.Testnet, + &exchange.HyperliquidWalletAddr, &exchange.AsterUser, &exchange.AsterSigner, &exchange.AsterPrivateKey, &exchange.CreatedAt, &exchange.UpdatedAt, ) diff --git a/decision/engine.go b/decision/engine.go index 76bcffca..1e990670 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -91,13 +91,18 @@ type FullDecision struct { // GetFullDecision 获取AI的完整交易决策(批量分析所有币种和持仓) func GetFullDecision(ctx *Context, mcpClient *mcp.Client) (*FullDecision, error) { + return GetFullDecisionWithCustomPrompt(ctx, mcpClient, "", false) +} + +// GetFullDecisionWithCustomPrompt 获取AI的完整交易决策(支持自定义prompt) +func GetFullDecisionWithCustomPrompt(ctx *Context, mcpClient *mcp.Client, customPrompt string, overrideBase bool) (*FullDecision, error) { // 1. 为所有币种获取市场数据 if err := fetchMarketDataForContext(ctx); err != nil { return nil, fmt.Errorf("获取市场数据失败: %w", err) } // 2. 构建 System Prompt(固定规则)和 User Prompt(动态数据) - systemPrompt := buildSystemPrompt(ctx.Account.TotalEquity, ctx.BTCETHLeverage, ctx.AltcoinLeverage) + systemPrompt := buildSystemPromptWithCustom(ctx.Account.TotalEquity, ctx.BTCETHLeverage, ctx.AltcoinLeverage, customPrompt, overrideBase) userPrompt := buildUserPrompt(ctx) // 3. 调用AI API(使用 system + user prompt) @@ -199,6 +204,33 @@ func calculateMaxCandidates(ctx *Context) int { return len(ctx.CandidateCoins) } +// buildSystemPromptWithCustom 构建包含自定义内容的 System Prompt +func buildSystemPromptWithCustom(accountEquity float64, btcEthLeverage, altcoinLeverage int, customPrompt string, overrideBase bool) string { + // 如果覆盖基础prompt且有自定义prompt,只使用自定义prompt + if overrideBase && customPrompt != "" { + return customPrompt + } + + // 获取基础prompt + basePrompt := buildSystemPrompt(accountEquity, btcEthLeverage, altcoinLeverage) + + // 如果没有自定义prompt,直接返回基础prompt + if customPrompt == "" { + return basePrompt + } + + // 添加自定义prompt部分到基础prompt + var sb strings.Builder + sb.WriteString(basePrompt) + sb.WriteString("\n\n") + sb.WriteString("# 📌 个性化交易策略\n\n") + sb.WriteString(customPrompt) + sb.WriteString("\n\n") + sb.WriteString("**注意**: 以上个性化策略是对基础规则的补充,不能违背基础风险控制原则。\n") + + return sb.String() +} + // buildSystemPrompt 构建 System Prompt(固定规则,可缓存) func buildSystemPrompt(accountEquity float64, btcEthLeverage, altcoinLeverage int) string { var sb strings.Builder diff --git a/go.mod b/go.mod index 0dbb9655..0c6dcfde 100644 --- a/go.mod +++ b/go.mod @@ -6,14 +6,19 @@ require ( github.com/adshao/go-binance/v2 v2.8.7 github.com/ethereum/go-ethereum v1.16.5 github.com/gin-gonic/gin v1.11.0 + github.com/golang-jwt/jwt/v5 v5.2.0 + github.com/google/uuid v1.6.0 github.com/mattn/go-sqlite3 v1.14.32 + github.com/pquerna/otp v1.4.0 github.com/sonirico/go-hyperliquid v0.17.0 + golang.org/x/crypto v0.42.0 ) require ( github.com/armon/go-radix v1.0.0 // indirect github.com/bitly/go-simplejson v0.5.0 // indirect github.com/bits-and-blooms/bitset v1.24.0 // indirect + github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect @@ -32,7 +37,6 @@ require ( github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/goccy/go-json v0.10.4 // indirect github.com/goccy/go-yaml v1.18.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/holiman/uint256 v1.3.2 // indirect github.com/joho/godotenv v1.5.1 // indirect @@ -66,7 +70,6 @@ require ( go.elastic.co/fastjson v1.5.1 // indirect go.uber.org/mock v0.5.0 // indirect golang.org/x/arch v0.20.0 // indirect - golang.org/x/crypto v0.42.0 // indirect golang.org/x/mod v0.27.0 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/sync v0.17.0 // indirect diff --git a/go.sum b/go.sum index 4bb6ff45..d0d7d69a 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/bits-and-blooms/bitset v1.24.0 h1:H4x4TuulnokZKvHLfzVRTHJfFfnHEeSYJiz github.com/bits-and-blooms/bitset v1.24.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= @@ -67,6 +69,8 @@ github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7Lk github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= @@ -132,6 +136,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= +github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= diff --git a/main.go b/main.go index 238a6d5c..a9ea34c9 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "nofx/api" + "nofx/auth" "nofx/config" "nofx/manager" "nofx/pool" @@ -38,6 +39,29 @@ func main() { useDefaultCoins := useDefaultCoinsStr == "true" apiPortStr, _ := database.GetSystemConfig("api_server_port") + // 获取管理员模式配置 + adminModeStr, _ := database.GetSystemConfig("admin_mode") + adminMode := adminModeStr != "false" // 默认为true + + // 设置JWT密钥 + jwtSecret, _ := database.GetSystemConfig("jwt_secret") + if jwtSecret == "" { + jwtSecret = "your-jwt-secret-key-change-in-production-make-it-long-and-random" + log.Printf("⚠️ 使用默认JWT密钥,建议在生产环境中配置") + } + auth.SetJWTSecret(jwtSecret) + + // 在管理员模式下,确保admin用户存在 + if adminMode { + err := database.EnsureAdminUser() + if err != nil { + log.Printf("⚠️ 创建admin用户失败: %v", err) + } else { + log.Printf("✓ 管理员模式已启用,无需登录") + } + auth.SetAdminMode(true) + } + log.Printf("✓ 配置数据库初始化成功") fmt.Println() @@ -73,8 +97,8 @@ func main() { log.Fatalf("❌ 加载交易员失败: %v", err) } - // 获取数据库中的所有交易员配置(用于显示) - traders, err := database.GetTraders() + // 获取数据库中的所有交易员配置(用于显示,使用default用户) + traders, err := database.GetTraders("default") if err != nil { log.Fatalf("❌ 获取交易员列表失败: %v", err) } @@ -110,7 +134,7 @@ func main() { fmt.Println() // 获取API服务器端口 - apiPort := 8081 // 默认端口 + apiPort := 8080 // 默认端口 if apiPortStr != "" { if port, err := strconv.Atoi(apiPortStr); err == nil { apiPort = port diff --git a/manager/trader_manager.go b/manager/trader_manager.go index 65cb92f0..31800fda 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -28,13 +28,20 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro tm.mu.Lock() defer tm.mu.Unlock() + // 根据admin_mode确定用户ID + adminModeStr, _ := database.GetSystemConfig("admin_mode") + userID := "default" + if adminModeStr != "false" { // 默认为true + userID = "admin" + } + // 获取数据库中的所有交易员 - traders, err := database.GetTraders() + traders, err := database.GetTraders(userID) if err != nil { return fmt.Errorf("获取交易员列表失败: %w", err) } - log.Printf("📋 加载数据库中的交易员配置: %d 个", len(traders)) + log.Printf("📋 加载数据库中的交易员配置: %d 个 (用户: %s)", len(traders), userID) // 获取系统配置 coinPoolURL, _ := database.GetSystemConfig("coin_pool_api_url") @@ -61,7 +68,7 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro // 为每个交易员获取AI模型和交易所配置 for _, traderCfg := range traders { // 获取AI模型配置 - aiModels, err := database.GetAIModels() + aiModels, err := database.GetAIModels(userID) if err != nil { log.Printf("⚠️ 获取AI模型配置失败: %v", err) continue @@ -86,7 +93,7 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro } // 获取交易所配置 - exchanges, err := database.GetExchanges() + exchanges, err := database.GetExchanges(userID) if err != nil { log.Printf("⚠️ 获取交易所配置失败: %v", err) continue @@ -155,6 +162,11 @@ func (tm *TraderManager) addTraderFromConfig(traderCfg *config.TraderConfig, aiM traderConfig.BinanceSecretKey = exchangeCfg.SecretKey } else if exchangeCfg.ID == "hyperliquid" { traderConfig.HyperliquidPrivateKey = exchangeCfg.APIKey // hyperliquid用APIKey存储private key + traderConfig.HyperliquidWalletAddr = exchangeCfg.HyperliquidWalletAddr + } else if exchangeCfg.ID == "aster" { + traderConfig.AsterUser = exchangeCfg.AsterUser + traderConfig.AsterSigner = exchangeCfg.AsterSigner + traderConfig.AsterPrivateKey = exchangeCfg.AsterPrivateKey } // 根据AI模型设置API密钥 @@ -169,6 +181,17 @@ func (tm *TraderManager) addTraderFromConfig(traderCfg *config.TraderConfig, aiM if err != nil { return fmt.Errorf("创建trader失败: %w", err) } + + // 设置自定义prompt(如果有) + if traderCfg.CustomPrompt != "" { + at.SetCustomPrompt(traderCfg.CustomPrompt) + at.SetOverrideBasePrompt(traderCfg.OverrideBasePrompt) + if traderCfg.OverrideBasePrompt { + log.Printf("✓ 已设置自定义交易策略prompt (覆盖基础prompt)") + } else { + log.Printf("✓ 已设置自定义交易策略prompt (补充基础prompt)") + } + } tm.traders[traderCfg.ID] = at log.Printf("✓ Trader '%s' (%s + %s) 已加载到内存", traderCfg.Name, aiModelCfg.Provider, exchangeCfg.ID) @@ -213,6 +236,11 @@ func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderConfig, aiModel traderConfig.BinanceSecretKey = exchangeCfg.SecretKey } else if exchangeCfg.ID == "hyperliquid" { traderConfig.HyperliquidPrivateKey = exchangeCfg.APIKey // hyperliquid用APIKey存储private key + traderConfig.HyperliquidWalletAddr = exchangeCfg.HyperliquidWalletAddr + } else if exchangeCfg.ID == "aster" { + traderConfig.AsterUser = exchangeCfg.AsterUser + traderConfig.AsterSigner = exchangeCfg.AsterSigner + traderConfig.AsterPrivateKey = exchangeCfg.AsterPrivateKey } // 根据AI模型设置API密钥 @@ -227,6 +255,17 @@ func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderConfig, aiModel if err != nil { return fmt.Errorf("创建trader失败: %w", err) } + + // 设置自定义prompt(如果有) + if traderCfg.CustomPrompt != "" { + at.SetCustomPrompt(traderCfg.CustomPrompt) + at.SetOverrideBasePrompt(traderCfg.OverrideBasePrompt) + if traderCfg.OverrideBasePrompt { + log.Printf("✓ 已设置自定义交易策略prompt (覆盖基础prompt)") + } else { + log.Printf("✓ 已设置自定义交易策略prompt (补充基础prompt)") + } + } tm.traders[traderCfg.ID] = at log.Printf("✓ Trader '%s' (%s + %s) 已添加", traderCfg.Name, aiModelCfg.Provider, exchangeCfg.ID) @@ -331,3 +370,236 @@ func (tm *TraderManager) GetComparisonData() (map[string]interface{}, error) { return comparison, nil } + +// GetCompetitionData 获取竞赛数据(特定用户的所有交易员) +func (tm *TraderManager) GetCompetitionData(userID string) (map[string]interface{}, error) { + tm.mu.RLock() + defer tm.mu.RUnlock() + + comparison := make(map[string]interface{}) + traders := make([]map[string]interface{}, 0) + + // 只获取该用户的交易员 + for traderID, t := range tm.traders { + // 检查trader是否属于该用户(通过ID前缀判断) + // 格式:userID_traderName + if !isUserTrader(traderID, userID) { + continue + } + + account, err := t.GetAccountInfo() + if err != nil { + log.Printf("⚠️ 获取交易员 %s 账户信息失败: %v", traderID, err) + continue + } + + status := t.GetStatus() + traders = append(traders, map[string]interface{}{ + "trader_id": t.GetID(), + "trader_name": t.GetName(), + "ai_model": t.GetAIModel(), + "total_equity": account["total_equity"], + "total_pnl": account["total_pnl"], + "total_pnl_pct": account["total_pnl_pct"], + "position_count": account["position_count"], + "margin_used_pct": account["margin_used_pct"], + "is_running": status["is_running"], + }) + } + + comparison["traders"] = traders + comparison["count"] = len(traders) + + return comparison, nil +} + +// isUserTrader 检查trader是否属于指定用户 +func isUserTrader(traderID, userID string) bool { + // trader ID格式: userID_traderName 或 randomUUID_modelName + // 为了兼容性,我们检查前缀 + if len(traderID) >= len(userID) && traderID[:len(userID)] == userID { + return true + } + // 对于老的default用户,所有没有明确用户前缀的都属于default + if userID == "default" && !containsUserPrefix(traderID) { + return true + } + return false +} + +// containsUserPrefix 检查trader ID是否包含用户前缀 +func containsUserPrefix(traderID string) bool { + // 检查是否包含邮箱格式的前缀(user@example.com_traderName) + for i, ch := range traderID { + if ch == '@' { + // 找到@符号,说明可能是email前缀 + return true + } + if ch == '_' && i > 0 { + // 找到下划线但前面没有@,可能是UUID或其他格式 + break + } + } + return false +} + +// LoadUserTraders 为特定用户加载交易员到内存 +func (tm *TraderManager) LoadUserTraders(database *config.Database, userID string) error { + tm.mu.Lock() + defer tm.mu.Unlock() + + // 获取指定用户的所有交易员 + traders, err := database.GetTraders(userID) + if err != nil { + return fmt.Errorf("获取用户 %s 的交易员列表失败: %w", userID, err) + } + + log.Printf("📋 为用户 %s 加载交易员配置: %d 个", userID, len(traders)) + + // 获取系统配置 + coinPoolURL, _ := database.GetSystemConfig("coin_pool_api_url") + maxDailyLossStr, _ := database.GetSystemConfig("max_daily_loss") + maxDrawdownStr, _ := database.GetSystemConfig("max_drawdown") + stopTradingMinutesStr, _ := database.GetSystemConfig("stop_trading_minutes") + + // 解析配置 + maxDailyLoss := 10.0 // 默认值 + if val, err := strconv.ParseFloat(maxDailyLossStr, 64); err == nil { + maxDailyLoss = val + } + + maxDrawdown := 20.0 // 默认值 + if val, err := strconv.ParseFloat(maxDrawdownStr, 64); err == nil { + maxDrawdown = val + } + + stopTradingMinutes := 60 // 默认值 + if val, err := strconv.Atoi(stopTradingMinutesStr); err == nil { + stopTradingMinutes = val + } + + // 为每个交易员获取AI模型和交易所配置 + for _, traderCfg := range traders { + // 检查是否已经加载过这个交易员 + if _, exists := tm.traders[traderCfg.ID]; exists { + log.Printf("⚠️ 交易员 %s 已经加载,跳过", traderCfg.Name) + continue + } + + // 获取AI模型配置(使用该用户的配置) + aiModels, err := database.GetAIModels(userID) + if err != nil { + log.Printf("⚠️ 获取用户 %s 的AI模型配置失败: %v", userID, err) + continue + } + + var aiModelCfg *config.AIModelConfig + for _, model := range aiModels { + if model.ID == traderCfg.AIModelID { + aiModelCfg = model + break + } + } + + if aiModelCfg == nil { + log.Printf("⚠️ 交易员 %s 的AI模型 %s 不存在,跳过", traderCfg.Name, traderCfg.AIModelID) + continue + } + + if !aiModelCfg.Enabled { + log.Printf("⚠️ 交易员 %s 的AI模型 %s 未启用,跳过", traderCfg.Name, traderCfg.AIModelID) + continue + } + + // 获取交易所配置(使用该用户的配置) + exchanges, err := database.GetExchanges(userID) + if err != nil { + log.Printf("⚠️ 获取用户 %s 的交易所配置失败: %v", userID, err) + continue + } + + var exchangeCfg *config.ExchangeConfig + for _, exchange := range exchanges { + if exchange.ID == traderCfg.ExchangeID { + exchangeCfg = exchange + break + } + } + + if exchangeCfg == nil { + log.Printf("⚠️ 交易员 %s 的交易所 %s 不存在,跳过", traderCfg.Name, traderCfg.ExchangeID) + continue + } + + if !exchangeCfg.Enabled { + log.Printf("⚠️ 交易员 %s 的交易所 %s 未启用,跳过", traderCfg.Name, traderCfg.ExchangeID) + continue + } + + // 使用现有的方法加载交易员 + err = tm.loadSingleTrader(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, maxDailyLoss, maxDrawdown, stopTradingMinutes) + if err != nil { + log.Printf("⚠️ 加载交易员 %s 失败: %v", traderCfg.Name, err) + } + } + + return nil +} + +// loadSingleTrader 加载单个交易员(从现有代码提取的公共逻辑) +func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderConfig, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int) error { + // 构建AutoTraderConfig + traderConfig := trader.AutoTraderConfig{ + ID: traderCfg.ID, + Name: traderCfg.Name, + AIModel: aiModelCfg.Provider, // 使用provider作为模型标识 + Exchange: exchangeCfg.ID, // 使用exchange ID + InitialBalance: traderCfg.InitialBalance, + ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute, + CoinPoolAPIURL: coinPoolURL, + MaxDailyLoss: maxDailyLoss, + MaxDrawdown: maxDrawdown, + StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute, + } + + // 根据交易所类型设置API密钥 + if exchangeCfg.ID == "binance" { + traderConfig.BinanceAPIKey = exchangeCfg.APIKey + traderConfig.BinanceSecretKey = exchangeCfg.SecretKey + } else if exchangeCfg.ID == "hyperliquid" { + traderConfig.HyperliquidPrivateKey = exchangeCfg.APIKey // hyperliquid用APIKey存储private key + traderConfig.HyperliquidWalletAddr = exchangeCfg.HyperliquidWalletAddr + } else if exchangeCfg.ID == "aster" { + traderConfig.AsterUser = exchangeCfg.AsterUser + traderConfig.AsterSigner = exchangeCfg.AsterSigner + traderConfig.AsterPrivateKey = exchangeCfg.AsterPrivateKey + } + + // 根据AI模型设置API密钥 + if aiModelCfg.Provider == "qwen" { + traderConfig.QwenKey = aiModelCfg.APIKey + } else if aiModelCfg.Provider == "deepseek" { + traderConfig.DeepSeekKey = aiModelCfg.APIKey + } + + // 创建trader实例 + at, err := trader.NewAutoTrader(traderConfig) + if err != nil { + return fmt.Errorf("创建trader失败: %w", err) + } + + // 设置自定义prompt(如果有) + if traderCfg.CustomPrompt != "" { + at.SetCustomPrompt(traderCfg.CustomPrompt) + at.SetOverrideBasePrompt(traderCfg.OverrideBasePrompt) + if traderCfg.OverrideBasePrompt { + log.Printf("✓ 已设置自定义交易策略prompt (覆盖基础prompt)") + } else { + log.Printf("✓ 已设置自定义交易策略prompt (补充基础prompt)") + } + } + + tm.traders[traderCfg.ID] = at + log.Printf("✓ Trader '%s' (%s + %s) 已为用户加载到内存", traderCfg.Name, aiModelCfg.Provider, exchangeCfg.ID) + return nil +} diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 42bc2e69..aed0dc79 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -77,6 +77,8 @@ type AutoTrader struct { decisionLogger *logger.DecisionLogger // 决策日志记录器 initialBalance float64 dailyPnL float64 + customPrompt string // 自定义交易策略prompt + overrideBasePrompt bool // 是否覆盖基础prompt lastResetTime time.Time stopUntil time.Time isRunning bool @@ -287,7 +289,7 @@ func (at *AutoTrader) runCycle() error { // 4. 调用AI获取完整决策 log.Println("🤖 正在请求AI分析并决策...") - decision, err := decision.GetFullDecision(ctx, at.mcpClient) + decision, err := decision.GetFullDecisionWithCustomPrompt(ctx, at.mcpClient, at.customPrompt, at.overrideBasePrompt) // 即使有错误,也保存思维链、决策和输入prompt(用于debug) if decision != nil { @@ -735,6 +737,16 @@ func (at *AutoTrader) GetAIModel() string { return at.aiModel } +// SetCustomPrompt 设置自定义交易策略prompt +func (at *AutoTrader) SetCustomPrompt(prompt string) { + at.customPrompt = prompt +} + +// SetOverrideBasePrompt 设置是否覆盖基础prompt +func (at *AutoTrader) SetOverrideBasePrompt(override bool) { + at.overrideBasePrompt = override +} + // GetDecisionLogger 获取决策日志记录器 func (at *AutoTrader) GetDecisionLogger() *logger.DecisionLogger { return at.decisionLogger @@ -871,14 +883,15 @@ func (at *AutoTrader) GetPositions() ([]map[string]interface{}, error) { leverage = int(lev) } - pnlPct := 0.0 - if side == "long" { - pnlPct = ((markPrice - entryPrice) / entryPrice) * 100 - } else { - pnlPct = ((entryPrice - markPrice) / entryPrice) * 100 - } - + // 计算占用保证金 marginUsed := (quantity * markPrice) / float64(leverage) + + // 计算盈亏百分比(基于保证金) + // 收益率 = 未实现盈亏 / 保证金 × 100% + pnlPct := 0.0 + if marginUsed > 0 { + pnlPct = (unrealizedPnl / marginUsed) * 100 + } result = append(result, map[string]interface{}{ "symbol": symbol, diff --git a/web/public/icons/aster.svg b/web/public/icons/aster.svg new file mode 100644 index 00000000..0ac6b2f8 --- /dev/null +++ b/web/public/icons/aster.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/public/icons/binance.svg b/web/public/icons/binance.svg new file mode 100644 index 00000000..1eab330d --- /dev/null +++ b/web/public/icons/binance.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/icons/deepseek.svg b/web/public/icons/deepseek.svg new file mode 100644 index 00000000..3fc23024 --- /dev/null +++ b/web/public/icons/deepseek.svg @@ -0,0 +1 @@ +DeepSeek \ No newline at end of file diff --git a/web/public/icons/hypeliquid.svg b/web/public/icons/hypeliquid.svg new file mode 100644 index 00000000..c9eb0bd0 --- /dev/null +++ b/web/public/icons/hypeliquid.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/public/icons/qwen.svg b/web/public/icons/qwen.svg new file mode 100644 index 00000000..33b3f645 --- /dev/null +++ b/web/public/icons/qwen.svg @@ -0,0 +1 @@ +Qwen \ No newline at end of file diff --git a/web/src/App.tsx b/web/src/App.tsx index b1a20464..050baf26 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -3,9 +3,14 @@ import useSWR from 'swr'; import { api } from './lib/api'; import { EquityChart } from './components/EquityChart'; import { AITradersPage } from './components/AITradersPage'; +import { LoginPage } from './components/LoginPage'; +import { RegisterPage } from './components/RegisterPage'; +import { CompetitionPage } from './components/CompetitionPage'; import AILearning from './components/AILearning'; import { LanguageProvider, useLanguage } from './contexts/LanguageContext'; +import { AuthProvider, useAuth } from './contexts/AuthContext'; import { t, type Language } from './i18n/translations'; +import { useSystemConfig } from './hooks/useSystemConfig'; import type { SystemStatus, AccountInfo, @@ -15,11 +20,34 @@ import type { TraderInfo, } from './types'; -type Page = 'traders' | 'trader'; +type Page = 'competition' | 'traders' | 'trader'; + +// 获取友好的AI模型名称 +function getModelDisplayName(modelId: string): string { + switch (modelId.toLowerCase()) { + case 'deepseek': + return 'DeepSeek'; + case 'qwen': + return 'Qwen'; + case 'claude': + return 'Claude'; + case 'gpt4': + case 'gpt-4': + return 'GPT-4'; + case 'gpt3.5': + case 'gpt-3.5': + return 'GPT-3.5'; + default: + return modelId.toUpperCase(); + } +} function App() { const { language, setLanguage } = useLanguage(); - const [currentPage, setCurrentPage] = useState('traders'); + const { user, token, logout, isLoading } = useAuth(); + const { config: systemConfig, loading: configLoading } = useSystemConfig(); + const [route, setRoute] = useState(window.location.pathname); + const [currentPage, setCurrentPage] = useState('competition'); const [selectedTraderId, setSelectedTraderId] = useState(); const [lastUpdate, setLastUpdate] = useState('--:--:--'); @@ -105,59 +133,118 @@ function App() { const selectedTrader = traders?.find((t) => t.trader_id === selectedTraderId); + // Handle routing + useEffect(() => { + const handlePopState = () => { + setRoute(window.location.pathname); + }; + window.addEventListener('popstate', handlePopState); + return () => window.removeEventListener('popstate', handlePopState); + }, []); + + // Show loading spinner while checking auth or config + if (isLoading || configLoading) { + return ( +
+
+
+ ⚡ +
+

加载中...

+
+
+ ); + } + + // If not in admin mode and not authenticated, show login/register pages + if (!systemConfig?.admin_mode && (!user || !token)) { + if (route === '/register') { + return ; + } + return ; + } + return (
{/* Header - Binance Style */}
-
- {/* Mobile: Two rows, Desktop: Single row */} -
- {/* Left: Logo and Title */} -
-
+
+
+ {/* Left - Logo and Title */} +
+
-

+

{t('appTitle', language)}

-

+

{t('subtitle', language)}

- - {/* Right: Controls - Wrap on mobile */} -
- {/* GitHub Link - Hidden on mobile, icon only on tablet */} - { - e.currentTarget.style.background = '#2B3139'; - e.currentTarget.style.color = '#EAECEF'; - e.currentTarget.style.borderColor = '#F0B90B'; - }} - onMouseLeave={(e) => { - e.currentTarget.style.background = '#1E2329'; - e.currentTarget.style.color = '#848E9C'; - e.currentTarget.style.borderColor = '#2B3139'; - }} + + {/* Center - Page Toggle (absolutely positioned) */} +
+ + + +
+ + {/* Right - Actions */} +
+ + {/* User Info - Only show if not in admin mode */} + {!systemConfig?.admin_mode && user && ( +
+
+ {user.email[0].toUpperCase()} +
+ {user.email} +
+ )} + + {/* Admin Mode Indicator */} + {systemConfig?.admin_mode && ( +
+ ⚡ 管理员模式 +
+ )} {/* Language Toggle */} -
+
- {/* Page Toggle */} -
+ {/* Logout Button - Only show if not in admin mode */} + {!systemConfig?.admin_mode && ( - -
- - {/* Trader Selector (only show on trader page) */} - {currentPage === 'trader' && traders && traders.length > 0 && ( - )} {/* Status Indicator (only show on trader page) */} {currentPage === 'trader' && status && (
- {currentPage === 'traders' ? ( - + {currentPage === 'competition' ? ( + + ) : currentPage === 'traders' ? ( + { + setSelectedTraderId(traderId); + setCurrentPage('trader'); + }} + /> ) : ( )} @@ -263,6 +331,30 @@ function App() {
@@ -278,8 +370,14 @@ function TraderDetailsPage({ decisions, lastUpdate, language, + traders, + selectedTraderId, + onTraderSelect, }: { selectedTrader?: TraderInfo; + traders?: TraderInfo[]; + selectedTraderId?: string; + onTraderSelect: (traderId: string) => void; status?: SystemStatus; account?: AccountInfo; positions?: Position[]; @@ -320,14 +418,35 @@ function TraderDetailsPage({
{/* Trader Header */}
-

- - 🤖 - - {selectedTrader.trader_name} -

+
+

+ + 🤖 + + {selectedTrader.trader_name} +

+ + {/* Trader Selector */} + {traders && traders.length > 0 && ( +
+ 切换交易员: + +
+ )} +
- AI Model: {selectedTrader.ai_model.toUpperCase()} + AI Model: {getModelDisplayName(selectedTrader.ai_model.split('_').pop() || selectedTrader.ai_model)} {status && ( <> @@ -669,11 +788,13 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua ); } -// Wrap App with LanguageProvider -export default function AppWithLanguage() { +// Wrap App with providers +export default function AppWithProviders() { return ( - + + + ); } diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx index 27e0986a..9f157652 100644 --- a/web/src/components/AITradersPage.tsx +++ b/web/src/components/AITradersPage.tsx @@ -4,8 +4,34 @@ import { api } from '../lib/api'; import type { TraderInfo, CreateTraderRequest, AIModel, Exchange } from '../types'; import { useLanguage } from '../contexts/LanguageContext'; import { t } from '../i18n/translations'; +import { getExchangeIcon } from './ExchangeIcons'; +import { getModelIcon } from './ModelIcons'; -export function AITradersPage() { +// 获取友好的AI模型名称 +function getModelDisplayName(modelId: string): string { + switch (modelId.toLowerCase()) { + case 'deepseek': + return 'DeepSeek'; + case 'qwen': + return 'Qwen'; + case 'claude': + return 'Claude'; + case 'gpt4': + case 'gpt-4': + return 'GPT-4'; + case 'gpt3.5': + case 'gpt-3.5': + return 'GPT-3.5'; + default: + return modelId.toUpperCase(); + } +} + +interface AITradersPageProps { + onTraderSelect?: (traderId: string) => void; +} + +export function AITradersPage({ onTraderSelect }: AITradersPageProps) { const { language } = useLanguage(); const [showCreateModal, setShowCreateModal] = useState(false); const [showModelModal, setShowModelModal] = useState(false); @@ -14,6 +40,8 @@ export function AITradersPage() { const [editingExchange, setEditingExchange] = useState(null); const [allModels, setAllModels] = useState([]); const [allExchanges, setAllExchanges] = useState([]); + const [supportedModels, setSupportedModels] = useState([]); + const [supportedExchanges, setSupportedExchanges] = useState([]); const { data: traders, mutate: mutateTraders } = useSWR( 'traders', @@ -25,22 +53,41 @@ export function AITradersPage() { useEffect(() => { const loadConfigs = async () => { try { - const [modelConfigs, exchangeConfigs] = await Promise.all([ + console.log('🔄 开始加载模型和交易所配置...'); + const [modelConfigs, exchangeConfigs, supportedModels, supportedExchanges] = await Promise.all([ api.getModelConfigs(), - api.getExchangeConfigs() + api.getExchangeConfigs(), + api.getSupportedModels(), + api.getSupportedExchanges() ]); + console.log('✅ 用户模型配置加载成功:', modelConfigs); + console.log('✅ 用户交易所配置加载成功:', exchangeConfigs); + console.log('✅ 支持的模型加载成功:', supportedModels); + console.log('✅ 支持的交易所加载成功:', supportedExchanges); setAllModels(modelConfigs); setAllExchanges(exchangeConfigs); + setSupportedModels(supportedModels); + setSupportedExchanges(supportedExchanges); } catch (error) { - console.error('Failed to load configs:', error); + console.error('❌ 加载配置失败:', error); } }; loadConfigs(); }, []); - // 只显示已配置的模型和交易所 - const configuredModels = allModels.filter(m => m.enabled && m.apiKey); - const configuredExchanges = allExchanges.filter(e => e.enabled && e.apiKey && (e.id === 'hyperliquid' || e.secretKey)); + // 显示所有用户的模型和交易所配置(用于调试) + const configuredModels = allModels || []; + const configuredExchanges = allExchanges || []; + + // 只在创建交易员时使用已启用且配置完整的 + const enabledModels = allModels?.filter(m => m.enabled && m.apiKey) || []; + const enabledExchanges = allExchanges?.filter(e => { + if (!e.enabled || !e.apiKey) return false; + // Hyperliquid 只需要私钥(作为apiKey),不需要secretKey + if (e.id === 'hyperliquid') return true; + // 其他交易所需要secretKey + return e.secretKey && e.secretKey.trim() !== ''; + }) || []; // 检查模型是否正在被运行中的交易员使用 const isModelInUse = (modelId: string) => { @@ -52,10 +99,10 @@ export function AITradersPage() { return traders?.some(t => t.exchange_id === exchangeId && t.is_running) || false; }; - const handleCreateTrader = async (modelId: string, exchangeId: string, name: string, initialBalance: number) => { + const handleCreateTrader = async (modelId: string, exchangeId: string, name: string, initialBalance: number, customPrompt?: string, overrideBase?: boolean) => { try { - const model = allModels.find(m => m.id === modelId); - const exchange = allExchanges.find(e => e.id === exchangeId); + const model = allModels?.find(m => m.id === modelId); + const exchange = allExchanges?.find(e => e.id === exchangeId); if (!model?.enabled) { alert(t('modelNotConfigured', language)); @@ -71,7 +118,9 @@ export function AITradersPage() { name, ai_model_id: modelId, exchange_id: exchangeId, - initial_balance: initialBalance + initial_balance: initialBalance, + custom_prompt: customPrompt, + override_base_prompt: overrideBase }; await api.createTrader(request); @@ -127,9 +176,9 @@ export function AITradersPage() { if (!confirm('确定要删除此AI模型配置吗?')) return; try { - const updatedModels = allModels.map(m => + const updatedModels = allModels?.map(m => m.id === modelId ? { ...m, apiKey: '', enabled: false } : m - ); + ) || []; const request = { models: Object.fromEntries( @@ -155,9 +204,27 @@ export function AITradersPage() { const handleSaveModelConfig = async (modelId: string, apiKey: string) => { try { - const updatedModels = allModels.map(m => - m.id === modelId ? { ...m, apiKey, enabled: true } : m - ); + // 找到要配置的模型(从supportedModels中) + const modelToUpdate = supportedModels?.find(m => m.id === modelId); + if (!modelToUpdate) { + alert('模型不存在'); + return; + } + + // 创建或更新用户的模型配置 + const existingModel = allModels?.find(m => m.id === modelId); + let updatedModels; + + if (existingModel) { + // 更新现有配置 + updatedModels = allModels?.map(m => + m.id === modelId ? { ...m, apiKey, enabled: true } : m + ) || []; + } else { + // 添加新配置 + const newModel = { ...modelToUpdate, apiKey, enabled: true }; + updatedModels = [...(allModels || []), newModel]; + } const request = { models: Object.fromEntries( @@ -172,7 +239,11 @@ export function AITradersPage() { }; await api.updateModelConfigs(request); - setAllModels(updatedModels); + + // 重新获取用户配置以确保数据同步 + const refreshedModels = await api.getModelConfigs(); + setAllModels(refreshedModels); + setShowModelModal(false); setEditingModel(null); } catch (error) { @@ -185,9 +256,9 @@ export function AITradersPage() { if (!confirm('确定要删除此交易所配置吗?')) return; try { - const updatedExchanges = allExchanges.map(e => + const updatedExchanges = allExchanges?.map(e => e.id === exchangeId ? { ...e, apiKey: '', secretKey: '', enabled: false } : e - ); + ) || []; const request = { exchanges: Object.fromEntries( @@ -213,11 +284,29 @@ export function AITradersPage() { } }; - const handleSaveExchangeConfig = async (exchangeId: string, apiKey: string, secretKey?: string, testnet?: boolean) => { + const handleSaveExchangeConfig = async (exchangeId: string, apiKey: string, secretKey?: string, testnet?: boolean, hyperliquidWalletAddr?: string, asterUser?: string, asterSigner?: string, asterPrivateKey?: string) => { try { - const updatedExchanges = allExchanges.map(e => - e.id === exchangeId ? { ...e, apiKey, secretKey, testnet, enabled: true } : e - ); + // 找到要配置的交易所(从supportedExchanges中) + const exchangeToUpdate = supportedExchanges?.find(e => e.id === exchangeId); + if (!exchangeToUpdate) { + alert('交易所不存在'); + return; + } + + // 创建或更新用户的交易所配置 + const existingExchange = allExchanges?.find(e => e.id === exchangeId); + let updatedExchanges; + + if (existingExchange) { + // 更新现有配置 + updatedExchanges = allExchanges?.map(e => + e.id === exchangeId ? { ...e, apiKey, secretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, enabled: true } : e + ) || []; + } else { + // 添加新配置 + const newExchange = { ...exchangeToUpdate, apiKey, secretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, enabled: true }; + updatedExchanges = [...(allExchanges || []), newExchange]; + } const request = { exchanges: Object.fromEntries( @@ -227,14 +316,22 @@ export function AITradersPage() { enabled: exchange.enabled, api_key: exchange.apiKey || '', secret_key: exchange.secretKey || '', - testnet: exchange.testnet || false + testnet: exchange.testnet || false, + hyperliquid_wallet_addr: exchange.hyperliquidWalletAddr || '', + aster_user: exchange.asterUser || '', + aster_signer: exchange.asterSigner || '', + aster_private_key: exchange.asterPrivateKey || '' } ]) ) }; await api.updateExchangeConfigs(request); - setAllExchanges(updatedExchanges); + + // 重新获取用户配置以确保数据同步 + const refreshedExchanges = await api.getExchangeConfigs(); + setAllExchanges(refreshedExchanges); + setShowExchangeModal(false); setEditingExchange(null); } catch (error) { @@ -339,21 +436,25 @@ export function AITradersPage() { onClick={() => handleModelClick(model.id)} >
-
- {model.name[0]} +
+ {getModelIcon(model.provider || model.id, { width: 32, height: 32 }) || ( +
+ {model.name[0]} +
+ )}
{model.name}
- {t('configured', language)} + {inUse ? '正在使用' : model.enabled ? '已启用' : '已配置'}
-
+
); })} @@ -384,21 +485,17 @@ export function AITradersPage() { onClick={() => handleExchangeClick(exchange.id)} >
-
- {exchange.name[0]} +
+ {getExchangeIcon(exchange.id, { width: 32, height: 32 })}
{exchange.name}
- {exchange.type.toUpperCase()} • {t('configured', language)} + {exchange.type.toUpperCase()} • {inUse ? '正在使用' : exchange.enabled ? '已启用' : '已配置'}
-
+
); })} @@ -429,7 +526,7 @@ export function AITradersPage() {
🤖 @@ -439,9 +536,9 @@ export function AITradersPage() { {trader.trader_name}
- {trader.ai_model.toUpperCase()} Model • {trader.exchange_id?.toUpperCase()} + {getModelDisplayName(trader.ai_model.split('_').pop() || trader.ai_model)} Model • {trader.exchange_id?.toUpperCase()}
@@ -463,7 +560,15 @@ export function AITradersPage() { {/* Actions */}
+ +
+ + {/* Advanced Settings Toggle */} +
+ +
+ + {/* Custom Prompt Field - Show when advanced is toggled */} + {showAdvanced && ( +
+ +