mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2025-12-06 13:54:41 +08:00
sync fork
This commit is contained in:
@@ -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
|
||||
|
||||
262
README.md
262
README.md
@@ -1,15 +1,16 @@
|
||||
# 🤖 NOFX - AI-Driven Crypto Futures Auto Trading Competition System
|
||||
# 🤖 NOFX - Multi-AI Model Automated Trading Platform
|
||||
|
||||
[](https://golang.org/)
|
||||
[](https://reactjs.org/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://sqlite.org/)
|
||||
[](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!**
|
||||
|
||||
|
||||
302
api/server.go
302
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)
|
||||
|
||||
138
config/config.go
138
config/config.go
@@ -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
|
||||
}
|
||||
390
config/database.go
Normal file
390
config/database.go
Normal file
@@ -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()
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# 后端服务
|
||||
backend:
|
||||
|
||||
1
go.mod
1
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
|
||||
)
|
||||
|
||||
|
||||
2
go.sum
2
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=
|
||||
|
||||
97
main.go
97
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交易系统!")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ WORKDIR /app
|
||||
COPY package*.json ./
|
||||
|
||||
# 安装依赖
|
||||
RUN npm ci --only=production=false
|
||||
RUN npm ci
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
166
web/src/App.tsx
166
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<Page>('competition');
|
||||
const [currentPage, setCurrentPage] = useState<Page>('traders');
|
||||
const [selectedTraderId, setSelectedTraderId] = useState<string | undefined>();
|
||||
const [lastUpdate, setLastUpdate] = useState<string>('--:--:--');
|
||||
|
||||
@@ -102,7 +102,8 @@ function App() {
|
||||
{/* Header - Binance Style */}
|
||||
<header className="glass sticky top-0 z-50 backdrop-blur-xl">
|
||||
<div className="max-w-[1920px] mx-auto px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="relative flex items-center">
|
||||
{/* Left - Logo and Title */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center text-xl" style={{ background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)' }}>
|
||||
⚡
|
||||
@@ -116,30 +117,48 @@ function App() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* GitHub Link */}
|
||||
<a
|
||||
href="https://github.com/tinkle-community/nofx"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
|
||||
style={{ background: '#1E2329', color: '#848E9C', border: '1px solid #2B3139' }}
|
||||
onMouseEnter={(e) => {
|
||||
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) */}
|
||||
<div className="absolute left-1/2 transform -translate-x-1/2 flex gap-1 rounded p-1" style={{ background: '#1E2329' }}>
|
||||
<button
|
||||
onClick={() => setCurrentPage('traders')}
|
||||
className={`px-4 py-2 rounded text-sm font-semibold transition-all`}
|
||||
style={currentPage === 'traders'
|
||||
? { background: '#F0B90B', color: '#000' }
|
||||
: { background: 'transparent', color: '#848E9C' }
|
||||
}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
|
||||
</svg>
|
||||
<span>GitHub</span>
|
||||
</a>
|
||||
{t('aiTraders', language)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('trader')}
|
||||
className={`px-4 py-2 rounded text-sm font-semibold transition-all`}
|
||||
style={currentPage === 'trader'
|
||||
? { background: '#F0B90B', color: '#000' }
|
||||
: { background: 'transparent', color: '#848E9C' }
|
||||
}
|
||||
>
|
||||
{t('tradingPanel', language)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Right - Actions */}
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
{/* Trader Selector (only show on trader page) */}
|
||||
{currentPage === 'trader' && traders && traders.length > 0 && (
|
||||
<select
|
||||
value={selectedTraderId}
|
||||
onChange={(e) => setSelectedTraderId(e.target.value)}
|
||||
className="rounded px-3 py-2 text-sm font-medium cursor-pointer transition-colors"
|
||||
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
>
|
||||
{traders.map((trader) => (
|
||||
<option key={trader.trader_id} value={trader.trader_id}>
|
||||
{trader.trader_name} ({trader.ai_model.toUpperCase()})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
|
||||
{/* Language Toggle */}
|
||||
<div className="flex gap-1 rounded p-1" style={{ background: '#1E2329' }}>
|
||||
@@ -165,48 +184,6 @@ function App() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Page Toggle */}
|
||||
<div className="flex gap-1 rounded p-1" style={{ background: '#1E2329' }}>
|
||||
<button
|
||||
onClick={() => setCurrentPage('competition')}
|
||||
className={`px-4 py-2 rounded text-sm font-semibold transition-all ${
|
||||
currentPage === 'competition' ? '' : ''
|
||||
}`}
|
||||
style={currentPage === 'competition'
|
||||
? { background: '#F0B90B', color: '#000' }
|
||||
: { background: 'transparent', color: '#848E9C' }
|
||||
}
|
||||
>
|
||||
{t('competition', language)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('trader')}
|
||||
className={`px-4 py-2 rounded text-sm font-semibold transition-all`}
|
||||
style={currentPage === 'trader'
|
||||
? { background: '#F0B90B', color: '#000' }
|
||||
: { background: 'transparent', color: '#848E9C' }
|
||||
}
|
||||
>
|
||||
{t('details', language)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Trader Selector (only show on trader page) */}
|
||||
{currentPage === 'trader' && traders && traders.length > 0 && (
|
||||
<select
|
||||
value={selectedTraderId}
|
||||
onChange={(e) => setSelectedTraderId(e.target.value)}
|
||||
className="rounded px-3 py-2 text-sm font-medium cursor-pointer transition-colors"
|
||||
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
>
|
||||
{traders.map((trader) => (
|
||||
<option key={trader.trader_id} value={trader.trader_id}>
|
||||
{trader.trader_name} ({trader.ai_model.toUpperCase()})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
|
||||
{/* Status Indicator (only show on trader page) */}
|
||||
{currentPage === 'trader' && status && (
|
||||
<div
|
||||
@@ -232,8 +209,8 @@ function App() {
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="max-w-[1920px] mx-auto px-6 py-6">
|
||||
{currentPage === 'competition' ? (
|
||||
<CompetitionPage />
|
||||
{currentPage === 'traders' ? (
|
||||
<AITradersPage />
|
||||
) : (
|
||||
<TraderDetailsPage
|
||||
selectedTrader={selectedTrader}
|
||||
@@ -253,30 +230,6 @@ function App() {
|
||||
<div className="max-w-[1920px] mx-auto px-6 py-6 text-center text-sm" style={{ color: '#5E6673' }}>
|
||||
<p>{t('footerTitle', language)}</p>
|
||||
<p className="mt-1">{t('footerWarning', language)}</p>
|
||||
<div className="mt-4 flex items-center justify-center gap-2">
|
||||
<a
|
||||
href="https://github.com/tinkle-community/nofx"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
|
||||
style={{ background: '#1E2329', color: '#848E9C', border: '1px solid #2B3139' }}
|
||||
onMouseEnter={(e) => {
|
||||
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';
|
||||
}}
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
|
||||
</svg>
|
||||
<span>Star on GitHub</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -290,7 +243,6 @@ function TraderDetailsPage({
|
||||
account,
|
||||
positions,
|
||||
decisions,
|
||||
stats,
|
||||
lastUpdate,
|
||||
language,
|
||||
}: {
|
||||
@@ -358,9 +310,9 @@ function TraderDetailsPage({
|
||||
{account && (
|
||||
<div className="mb-4 p-3 rounded text-xs font-mono" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
||||
<div style={{ color: '#848E9C' }}>
|
||||
🔄 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'}%)
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -369,20 +321,20 @@ function TraderDetailsPage({
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||||
<StatCard
|
||||
title={t('totalEquity', language)}
|
||||
value={`${account?.total_equity.toFixed(2) || '0.00'} USDT`}
|
||||
value={`${account?.total_equity?.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}
|
||||
/>
|
||||
<StatCard
|
||||
title={t('availableBalance', language)}
|
||||
value={`${account?.available_balance.toFixed(2) || '0.00'} USDT`}
|
||||
subtitle={`${((account?.available_balance / account?.total_equity) * 100 || 0).toFixed(1)}% ${t('free', language)}`}
|
||||
value={`${account?.available_balance?.toFixed(2) || '0.00'} USDT`}
|
||||
subtitle={`${((account?.available_balance && account?.total_equity ? (account.available_balance / account.total_equity) * 100 : 0)).toFixed(1)}% ${t('free', language)}`}
|
||||
/>
|
||||
<StatCard
|
||||
title={t('totalPnL', language)}
|
||||
value={`${account?.total_pnl >= 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}
|
||||
/>
|
||||
<StatCard
|
||||
title={t('positions', language)}
|
||||
@@ -584,7 +536,7 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua
|
||||
</div>
|
||||
|
||||
{/* AI Input Prompt - Collapsible */}
|
||||
{decision.input_prompt && (
|
||||
{(decision as any).input_prompt && (
|
||||
<div className="mb-3">
|
||||
<button
|
||||
onClick={() => setShowInput(!showInput)}
|
||||
@@ -596,7 +548,7 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua
|
||||
</button>
|
||||
{showInput && (
|
||||
<div className="mt-2 rounded p-4 text-sm font-mono whitespace-pre-wrap max-h-96 overflow-y-auto" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}>
|
||||
{decision.input_prompt}
|
||||
{(decision as any).input_prompt}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
998
web/src/components/AITradersPage.tsx
Normal file
998
web/src/components/AITradersPage.tsx
Normal file
@@ -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<string | null>(null);
|
||||
const [editingExchange, setEditingExchange] = useState<string | null>(null);
|
||||
const [allModels, setAllModels] = useState<AIModel[]>([]);
|
||||
const [allExchanges, setAllExchanges] = useState<Exchange[]>([]);
|
||||
|
||||
const { data: traders, mutate: mutateTraders } = useSWR<TraderInfo[]>(
|
||||
'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 (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl flex items-center justify-center text-2xl" style={{
|
||||
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
||||
boxShadow: '0 4px 14px rgba(240, 185, 11, 0.4)'
|
||||
}}>
|
||||
🤖
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||
{t('aiTraders', language)}
|
||||
<span className="text-xs font-normal px-2 py-1 rounded" style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
color: '#F0B90B'
|
||||
}}>
|
||||
{traders?.length || 0} {t('active', language)}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('manageAITraders', language)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handleAddModel}
|
||||
className="px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
|
||||
style={{
|
||||
background: '#2B3139',
|
||||
color: '#EAECEF',
|
||||
border: '1px solid #474D57'
|
||||
}}
|
||||
>
|
||||
➕ {t('aiModels', language)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleAddExchange}
|
||||
className="px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
|
||||
style={{
|
||||
background: '#2B3139',
|
||||
color: '#EAECEF',
|
||||
border: '1px solid #474D57'
|
||||
}}
|
||||
>
|
||||
➕ {t('exchanges', language)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
disabled={configuredModels.length === 0 || configuredExchanges.length === 0}
|
||||
className="px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: (configuredModels.length > 0 && configuredExchanges.length > 0) ? '#F0B90B' : '#2B3139',
|
||||
color: (configuredModels.length > 0 && configuredExchanges.length > 0) ? '#000' : '#848E9C'
|
||||
}}
|
||||
>
|
||||
➕ {t('createTrader', language)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Configuration Status */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* AI Models */}
|
||||
<div className="binance-card p-4">
|
||||
<h3 className="text-lg font-semibold mb-3" style={{ color: '#EAECEF' }}>
|
||||
🧠 {t('aiModels', language)}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{configuredModels.map(model => {
|
||||
const inUse = isModelInUse(model.id);
|
||||
return (
|
||||
<div
|
||||
key={model.id}
|
||||
className={`flex items-center justify-between p-3 rounded transition-all ${
|
||||
inUse ? 'cursor-not-allowed' : 'cursor-pointer hover:bg-gray-700'
|
||||
}`}
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||
onClick={() => handleModelClick(model.id)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold"
|
||||
style={{
|
||||
background: model.id === 'deepseek' ? '#60a5fa' : '#c084fc',
|
||||
color: '#fff'
|
||||
}}>
|
||||
{model.name[0]}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold" style={{ color: '#EAECEF' }}>{model.name}</div>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('configured', language)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`w-3 h-3 rounded-full bg-green-400`} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{configuredModels.length === 0 && (
|
||||
<div className="text-center py-8" style={{ color: '#848E9C' }}>
|
||||
<div className="text-2xl mb-2">🧠</div>
|
||||
<div className="text-sm">暂无已配置的AI模型</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Exchanges */}
|
||||
<div className="binance-card p-4">
|
||||
<h3 className="text-lg font-semibold mb-3" style={{ color: '#EAECEF' }}>
|
||||
🏦 {t('exchanges', language)}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{configuredExchanges.map(exchange => {
|
||||
const inUse = isExchangeInUse(exchange.id);
|
||||
return (
|
||||
<div
|
||||
key={exchange.id}
|
||||
className={`flex items-center justify-between p-3 rounded transition-all ${
|
||||
inUse ? 'cursor-not-allowed' : 'cursor-pointer hover:bg-gray-700'
|
||||
}`}
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||
onClick={() => handleExchangeClick(exchange.id)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold"
|
||||
style={{
|
||||
background: exchange.type === 'cex' ? '#F0B90B' : '#0ECB81',
|
||||
color: '#000'
|
||||
}}>
|
||||
{exchange.name[0]}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold" style={{ color: '#EAECEF' }}>{exchange.name}</div>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{exchange.type.toUpperCase()} • {t('configured', language)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`w-3 h-3 rounded-full bg-green-400`} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{configuredExchanges.length === 0 && (
|
||||
<div className="text-center py-8" style={{ color: '#848E9C' }}>
|
||||
<div className="text-2xl mb-2">🏦</div>
|
||||
<div className="text-sm">暂无已配置的交易所</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Traders List */}
|
||||
<div className="binance-card p-6">
|
||||
<div className="flex items-center justify-between mb-5">
|
||||
<h2 className="text-xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||
👥 {t('currentTraders', language)}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{traders && traders.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{traders.map(trader => (
|
||||
<div key={trader.trader_id}
|
||||
className="flex items-center justify-between p-4 rounded transition-all hover:translate-y-[-1px]"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-full flex items-center justify-center text-xl"
|
||||
style={{
|
||||
background: trader.ai_model === 'deepseek' ? '#60a5fa' : '#c084fc',
|
||||
color: '#fff'
|
||||
}}>
|
||||
🤖
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-lg" style={{ color: '#EAECEF' }}>
|
||||
{trader.trader_name}
|
||||
</div>
|
||||
<div className="text-sm" style={{
|
||||
color: trader.ai_model === 'deepseek' ? '#60a5fa' : '#c084fc'
|
||||
}}>
|
||||
{trader.ai_model.toUpperCase()} Model • {trader.exchange_id?.toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Status */}
|
||||
<div className="text-center">
|
||||
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>{t('status', language)}</div>
|
||||
<div className={`px-3 py-1 rounded text-xs font-bold ${
|
||||
trader.is_running ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||
}`} style={trader.is_running
|
||||
? { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81' }
|
||||
: { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }
|
||||
}>
|
||||
{trader.is_running ? t('running', language) : t('stopped', language)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleToggleTrader(trader.trader_id, trader.is_running)}
|
||||
className="px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
|
||||
style={trader.is_running
|
||||
? { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }
|
||||
: { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81' }
|
||||
}
|
||||
>
|
||||
{trader.is_running ? t('stop', language) : t('start', language)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleDeleteTrader(trader.trader_id)}
|
||||
className="px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
|
||||
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-16" style={{ color: '#848E9C' }}>
|
||||
<div className="text-6xl mb-4 opacity-50">🤖</div>
|
||||
<div className="text-lg font-semibold mb-2">{t('noTraders', language)}</div>
|
||||
<div className="text-sm mb-4">{t('createFirstTrader', language)}</div>
|
||||
{(configuredModels.length === 0 || configuredExchanges.length === 0) && (
|
||||
<div className="text-sm text-yellow-500">
|
||||
{configuredModels.length === 0 && configuredExchanges.length === 0
|
||||
? t('configureModelsAndExchangesFirst', language)
|
||||
: configuredModels.length === 0
|
||||
? t('configureModelsFirst', language)
|
||||
: t('configureExchangesFirst', language)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Create Trader Modal */}
|
||||
{showCreateModal && (
|
||||
<CreateTraderModal
|
||||
enabledModels={configuredModels}
|
||||
enabledExchanges={configuredExchanges}
|
||||
onCreate={handleCreateTrader}
|
||||
onClose={() => setShowCreateModal(false)}
|
||||
language={language}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Model Configuration Modal */}
|
||||
{showModelModal && (
|
||||
<ModelConfigModal
|
||||
allModels={allModels}
|
||||
editingModelId={editingModel}
|
||||
onSave={handleSaveModelConfig}
|
||||
onDelete={handleDeleteModelConfig}
|
||||
onClose={() => {
|
||||
setShowModelModal(false);
|
||||
setEditingModel(null);
|
||||
}}
|
||||
language={language}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Exchange Configuration Modal */}
|
||||
{showExchangeModal && (
|
||||
<ExchangeConfigModal
|
||||
allExchanges={allExchanges}
|
||||
editingExchangeId={editingExchange}
|
||||
onSave={handleSaveExchangeConfig}
|
||||
onDelete={handleDeleteExchangeConfig}
|
||||
onClose={() => {
|
||||
setShowExchangeModal(false);
|
||||
setEditingExchange(null);
|
||||
}}
|
||||
language={language}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-gray-800 rounded-lg p-6 w-full max-w-lg max-h-[90vh] overflow-y-auto" style={{ background: '#1E2329' }}>
|
||||
<h3 className="text-xl font-bold mb-4" style={{ color: '#EAECEF' }}>
|
||||
{t('createNewTrader', language)}
|
||||
</h3>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
{t('selectAIModel', language)}
|
||||
</label>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
required
|
||||
>
|
||||
{enabledModels.map(model => (
|
||||
<option key={model.id} value={model.id}>
|
||||
{model.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
{t('traderName', language)}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={traderName}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
{t('selectExchange', language)}
|
||||
</label>
|
||||
<select
|
||||
value={selectedExchange}
|
||||
onChange={(e) => setSelectedExchange(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
required
|
||||
>
|
||||
{enabledExchanges.map(exchange => (
|
||||
<option key={exchange.id} value={exchange.id}>
|
||||
{exchange.name} ({exchange.type.toUpperCase()})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
初始资金 (USDT)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={initialBalance}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 mt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="flex-1 px-4 py-2 rounded text-sm font-semibold"
|
||||
style={{ background: '#2B3139', color: '#848E9C' }}
|
||||
>
|
||||
{t('cancel', language)}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 px-4 py-2 rounded text-sm font-semibold"
|
||||
style={{ background: '#F0B90B', color: '#000' }}
|
||||
>
|
||||
{t('create', language)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-gray-800 rounded-lg p-6 w-full max-w-lg" style={{ background: '#1E2329' }}>
|
||||
<h3 className="text-xl font-bold mb-4" style={{ color: '#EAECEF' }}>
|
||||
{editingModelId ? '编辑AI模型' : '添加AI模型'}
|
||||
</h3>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{!editingModelId && (
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
选择AI模型
|
||||
</label>
|
||||
<select
|
||||
value={selectedModelId}
|
||||
onChange={(e) => setSelectedModelId(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
required
|
||||
>
|
||||
<option value="">请选择模型</option>
|
||||
{availableModels.map(model => (
|
||||
<option key={model.id} value={model.id}>
|
||||
{model.name} ({model.provider})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedModel && (
|
||||
<div className="p-4 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold"
|
||||
style={{
|
||||
background: selectedModel.id === 'deepseek' ? '#60a5fa' : '#c084fc',
|
||||
color: '#fff'
|
||||
}}>
|
||||
{selectedModel.name[0]}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold" style={{ color: '#EAECEF' }}>{selectedModel.name}</div>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>{selectedModel.provider}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
API Key
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3 mt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="flex-1 px-4 py-2 rounded text-sm font-semibold"
|
||||
style={{ background: '#2B3139', color: '#848E9C' }}
|
||||
>
|
||||
{t('cancel', language)}
|
||||
</button>
|
||||
{editingModelId && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onDelete(editingModelId);
|
||||
}}
|
||||
className="px-4 py-2 rounded text-sm font-semibold"
|
||||
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!selectedModelId || !apiKey.trim()}
|
||||
className="flex-1 px-4 py-2 rounded text-sm font-semibold disabled:opacity-50"
|
||||
style={{ background: '#F0B90B', color: '#000' }}
|
||||
>
|
||||
{t('save', language)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-gray-800 rounded-lg p-6 w-full max-w-lg" style={{ background: '#1E2329' }}>
|
||||
<h3 className="text-xl font-bold mb-4" style={{ color: '#EAECEF' }}>
|
||||
{editingExchangeId ? '编辑交易所' : '添加交易所'}
|
||||
</h3>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{!editingExchangeId && (
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
选择交易所
|
||||
</label>
|
||||
<select
|
||||
value={selectedExchangeId}
|
||||
onChange={(e) => setSelectedExchangeId(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
required
|
||||
>
|
||||
<option value="">请选择交易所</option>
|
||||
{availableExchanges.map(exchange => (
|
||||
<option key={exchange.id} value={exchange.id}>
|
||||
{exchange.name} ({exchange.type.toUpperCase()})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedExchange && (
|
||||
<div className="p-4 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold"
|
||||
style={{
|
||||
background: selectedExchange.type === 'cex' ? '#F0B90B' : '#0ECB81',
|
||||
color: '#000'
|
||||
}}>
|
||||
{selectedExchange.name[0]}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold" style={{ color: '#EAECEF' }}>{selectedExchange.name}</div>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>{selectedExchange.type.toUpperCase()}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
{selectedExchange.id === 'hyperliquid' ? 'Private Key (无需0x前缀)' : 'API Key'}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedExchange.id !== 'hyperliquid' && (
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
Secret Key
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={secretKey}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedExchange.type === 'dex' && (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={testnet}
|
||||
onChange={(e) => setTestnet(e.target.checked)}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<label className="text-sm" style={{ color: '#EAECEF' }}>
|
||||
{t('useTestnet', language)}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3 mt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="flex-1 px-4 py-2 rounded text-sm font-semibold"
|
||||
style={{ background: '#2B3139', color: '#848E9C' }}
|
||||
>
|
||||
{t('cancel', language)}
|
||||
</button>
|
||||
{editingExchangeId && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onDelete(editingExchangeId);
|
||||
}}
|
||||
className="px-4 py-2 rounded text-sm font-semibold"
|
||||
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!selectedExchangeId || !apiKey.trim() || (selectedExchange?.id !== 'hyperliquid' && !secretKey.trim())}
|
||||
className="flex-1 px-4 py-2 rounded text-sm font-semibold disabled:opacity-50"
|
||||
style={{ background: '#F0B90B', color: '#000' }}
|
||||
>
|
||||
{t('save', language)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<string, {
|
||||
timestamp: string;
|
||||
time: string;
|
||||
traders: Map<string, { pnl_pct: number; equity: number }>;
|
||||
}>();
|
||||
|
||||
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 (
|
||||
<div className="text-center py-16" style={{ color: '#848E9C' }}>
|
||||
<div className="spinner mx-auto mb-4"></div>
|
||||
<div className="text-sm font-semibold">Loading comparison data...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (combinedData.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-16" style={{ color: '#848E9C' }}>
|
||||
<div className="text-6xl mb-4 opacity-50">📊</div>
|
||||
<div className="text-lg font-semibold mb-2">暂无历史数据</div>
|
||||
<div className="text-sm">运行几个周期后将显示对比曲线</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 限制显示数据点
|
||||
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 (
|
||||
<div className="rounded p-3 shadow-xl" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
||||
<div className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{data.time} - #{data.index}
|
||||
</div>
|
||||
{traders.map((trader) => {
|
||||
const pnlPct = data[`${trader.trader_id}_pnl_pct`];
|
||||
const equity = data[`${trader.trader_id}_equity`];
|
||||
if (pnlPct === undefined) return null;
|
||||
|
||||
return (
|
||||
<div key={trader.trader_id} className="mb-1.5 last:mb-0">
|
||||
<div
|
||||
className="text-xs font-semibold mb-0.5"
|
||||
style={{ color: getTraderColor(trader.trader_id) }}
|
||||
>
|
||||
{trader.trader_name}
|
||||
</div>
|
||||
<div className="text-sm mono font-bold" style={{ color: pnlPct >= 0 ? '#0ECB81' : '#F6465D' }}>
|
||||
{pnlPct >= 0 ? '+' : ''}{pnlPct.toFixed(2)}%
|
||||
<span className="text-xs ml-2 font-normal" style={{ color: '#848E9C' }}>
|
||||
({equity?.toFixed(2)} USDT)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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 (
|
||||
<div>
|
||||
<div style={{ borderRadius: '8px', overflow: 'hidden' }}>
|
||||
<ResponsiveContainer width="100%" height={520}>
|
||||
<LineChart data={displayData} margin={{ top: 20, right: 30, left: 20, bottom: 40 }}>
|
||||
<defs>
|
||||
{traders.map((trader) => (
|
||||
<linearGradient
|
||||
key={`gradient-${trader.trader_id}`}
|
||||
id={`gradient-${trader.trader_id}`}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop offset="5%" stopColor={getTraderColor(trader.trader_id)} stopOpacity={0.9} />
|
||||
<stop offset="95%" stopColor={getTraderColor(trader.trader_id)} stopOpacity={0.2} />
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#2B3139" />
|
||||
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
stroke="#5E6673"
|
||||
tick={{ fill: '#848E9C', fontSize: 11 }}
|
||||
tickLine={{ stroke: '#2B3139' }}
|
||||
interval={Math.floor(displayData.length / 12)}
|
||||
angle={-15}
|
||||
textAnchor="end"
|
||||
height={60}
|
||||
/>
|
||||
|
||||
<YAxis
|
||||
stroke="#5E6673"
|
||||
tick={{ fill: '#848E9C', fontSize: 12 }}
|
||||
tickLine={{ stroke: '#2B3139' }}
|
||||
domain={calculateYDomain()}
|
||||
tickFormatter={(value) => `${value.toFixed(1)}%`}
|
||||
width={60}
|
||||
/>
|
||||
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
|
||||
<ReferenceLine
|
||||
y={0}
|
||||
stroke="#474D57"
|
||||
strokeDasharray="5 5"
|
||||
strokeWidth={1.5}
|
||||
label={{
|
||||
value: 'Break Even',
|
||||
fill: '#848E9C',
|
||||
fontSize: 11,
|
||||
position: 'right',
|
||||
}}
|
||||
/>
|
||||
|
||||
{traders.map((trader, index) => (
|
||||
<Line
|
||||
key={trader.trader_id}
|
||||
type="monotone"
|
||||
dataKey={`${trader.trader_id}_pnl_pct`}
|
||||
stroke={getTraderColor(trader.trader_id)}
|
||||
strokeWidth={3}
|
||||
dot={displayData.length < 50 ? { fill: getTraderColor(trader.trader_id), r: 3 } : false}
|
||||
activeDot={{ r: 6, fill: getTraderColor(trader.trader_id), stroke: '#fff', strokeWidth: 2 }}
|
||||
name={trader.trader_name}
|
||||
connectNulls
|
||||
/>
|
||||
))}
|
||||
|
||||
<Legend
|
||||
wrapperStyle={{ paddingTop: '20px' }}
|
||||
iconType="line"
|
||||
formatter={(value, entry: any) => {
|
||||
const traderId = traders.find((t) => value === t.trader_name)?.trader_id;
|
||||
const trader = traders.find((t) => t.trader_id === traderId);
|
||||
return (
|
||||
<span style={{ color: entry.color, fontWeight: 600, fontSize: '14px' }}>
|
||||
{trader?.trader_name} ({trader?.ai_model.toUpperCase()})
|
||||
</span>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="mt-6 grid grid-cols-4 gap-4 pt-5" style={{ borderTop: '1px solid #2B3139' }}>
|
||||
<div className="p-3 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
|
||||
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>对比模式</div>
|
||||
<div className="text-base font-bold" style={{ color: '#EAECEF' }}>PnL %</div>
|
||||
</div>
|
||||
<div className="p-3 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
|
||||
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>数据点数</div>
|
||||
<div className="text-base font-bold mono" style={{ color: '#EAECEF' }}>{combinedData.length} 个</div>
|
||||
</div>
|
||||
<div className="p-3 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
|
||||
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>当前差距</div>
|
||||
<div className="text-base font-bold mono" style={{ color: currentGap > 1 ? '#F0B90B' : '#EAECEF' }}>
|
||||
{currentGap.toFixed(2)}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
|
||||
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>显示范围</div>
|
||||
<div className="text-base font-bold mono" style={{ color: '#EAECEF' }}>
|
||||
{combinedData.length > MAX_DISPLAY_POINTS
|
||||
? `最近 ${MAX_DISPLAY_POINTS}`
|
||||
: '全部数据'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<CompetitionData>(
|
||||
'competition',
|
||||
api.getCompetition,
|
||||
{
|
||||
refreshInterval: 5000,
|
||||
revalidateOnFocus: true,
|
||||
}
|
||||
);
|
||||
|
||||
if (!competition || !competition.traders) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="binance-card p-8 animate-pulse">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="space-y-3 flex-1">
|
||||
<div className="skeleton h-8 w-64"></div>
|
||||
<div className="skeleton h-4 w-48"></div>
|
||||
</div>
|
||||
<div className="skeleton h-12 w-32"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="binance-card p-6">
|
||||
<div className="skeleton h-6 w-40 mb-4"></div>
|
||||
<div className="space-y-3">
|
||||
<div className="skeleton h-20 w-full rounded"></div>
|
||||
<div className="skeleton h-20 w-full rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 按收益率排序
|
||||
const sortedTraders = [...competition.traders].sort(
|
||||
(a, b) => b.total_pnl_pct - a.total_pnl_pct
|
||||
);
|
||||
|
||||
// 找出领先者
|
||||
const leader = sortedTraders[0];
|
||||
|
||||
return (
|
||||
<div className="space-y-5 animate-fade-in">
|
||||
{/* Competition Header - 精简版 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl flex items-center justify-center text-2xl" style={{
|
||||
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
||||
boxShadow: '0 4px 14px rgba(240, 185, 11, 0.4)'
|
||||
}}>
|
||||
🏆
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||
{t('aiCompetition', language)}
|
||||
<span className="text-xs font-normal px-2 py-1 rounded" style={{ background: 'rgba(240, 185, 11, 0.15)', color: '#F0B90B' }}>
|
||||
{competition.count} {t('traders', language)}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('liveBattle', language)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>{t('leader', language)}</div>
|
||||
<div className="text-lg font-bold" style={{ color: '#F0B90B' }}>{leader?.trader_name}</div>
|
||||
<div className="text-sm font-semibold" style={{ color: leader.total_pnl >= 0 ? '#0ECB81' : '#F6465D' }}>
|
||||
{leader.total_pnl >= 0 ? '+' : ''}{leader.total_pnl_pct.toFixed(2)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Left/Right Split: Performance Chart + Leaderboard */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
{/* Left: Performance Comparison Chart */}
|
||||
<div className="binance-card p-5 animate-slide-in" style={{ animationDelay: '0.1s' }}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||
{t('performanceComparison', language)}
|
||||
</h2>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('realTimePnL', language)}
|
||||
</div>
|
||||
</div>
|
||||
<ComparisonChart traders={sortedTraders} />
|
||||
</div>
|
||||
|
||||
{/* Right: Leaderboard */}
|
||||
<div className="binance-card p-5 animate-slide-in" style={{ animationDelay: '0.1s' }}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||
{t('leaderboard', language)}
|
||||
</h2>
|
||||
<div className="text-xs px-2 py-1 rounded" style={{ background: 'rgba(240, 185, 11, 0.1)', color: '#F0B90B', border: '1px solid rgba(240, 185, 11, 0.2)' }}>
|
||||
{t('live', language)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{sortedTraders.map((trader, index) => {
|
||||
const isLeader = index === 0;
|
||||
const aiModelColor = trader.ai_model === 'qwen' ? '#c084fc' : '#60a5fa';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={trader.trader_id}
|
||||
className="rounded p-3 transition-all duration-300 hover:translate-y-[-1px]"
|
||||
style={{
|
||||
background: isLeader ? 'linear-gradient(135deg, rgba(240, 185, 11, 0.08) 0%, #0B0E11 100%)' : '#0B0E11',
|
||||
border: `1px solid ${isLeader ? 'rgba(240, 185, 11, 0.4)' : '#2B3139'}`,
|
||||
boxShadow: isLeader ? '0 3px 15px rgba(240, 185, 11, 0.12), 0 0 0 1px rgba(240, 185, 11, 0.15)' : '0 1px 4px rgba(0, 0, 0, 0.3)'
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Rank & Name */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-2xl w-6">
|
||||
{index === 0 ? '🥇' : index === 1 ? '🥈' : '🥉'}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-sm" style={{ color: '#EAECEF' }}>{trader.trader_name}</div>
|
||||
<div className="text-xs mono font-semibold" style={{ color: aiModelColor }}>
|
||||
{trader.ai_model.toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Total Equity */}
|
||||
<div className="text-right">
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>{t('equity', language)}</div>
|
||||
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
|
||||
{trader.total_equity.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* P&L */}
|
||||
<div className="text-right min-w-[90px]">
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>{t('pnl', language)}</div>
|
||||
<div
|
||||
className="text-lg font-bold mono"
|
||||
style={{ color: trader.total_pnl >= 0 ? '#0ECB81' : '#F6465D' }}
|
||||
>
|
||||
{trader.total_pnl >= 0 ? '+' : ''}
|
||||
{trader.total_pnl_pct.toFixed(2)}%
|
||||
</div>
|
||||
<div className="text-xs mono" style={{ color: '#848E9C' }}>
|
||||
{trader.total_pnl >= 0 ? '+' : ''}{trader.total_pnl.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Positions */}
|
||||
<div className="text-right">
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>{t('pos', language)}</div>
|
||||
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
|
||||
{trader.position_count}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{trader.margin_used_pct.toFixed(1)}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div>
|
||||
<div
|
||||
className="px-2 py-1 rounded text-xs font-bold"
|
||||
style={trader.is_running
|
||||
? { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81' }
|
||||
: { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }
|
||||
}
|
||||
>
|
||||
{trader.is_running ? '●' : '○'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Head-to-Head Stats */}
|
||||
{competition.traders.length === 2 && (
|
||||
<div className="binance-card p-5 animate-slide-in" style={{ animationDelay: '0.3s' }}>
|
||||
<h2 className="text-lg font-bold mb-4 flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||
{t('headToHead', language)}
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{sortedTraders.map((trader, index) => {
|
||||
const isWinning = index === 0;
|
||||
const opponent = sortedTraders[1 - index];
|
||||
const gap = trader.total_pnl_pct - opponent.total_pnl_pct;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={trader.trader_id}
|
||||
className="p-4 rounded transition-all duration-300 hover:scale-[1.02]"
|
||||
style={isWinning
|
||||
? {
|
||||
background: 'linear-gradient(135deg, rgba(14, 203, 129, 0.08) 0%, rgba(14, 203, 129, 0.02) 100%)',
|
||||
border: '2px solid rgba(14, 203, 129, 0.3)',
|
||||
boxShadow: '0 3px 15px rgba(14, 203, 129, 0.12)'
|
||||
}
|
||||
: {
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
boxShadow: '0 1px 4px rgba(0, 0, 0, 0.3)'
|
||||
}
|
||||
}
|
||||
>
|
||||
<div className="text-center">
|
||||
<div
|
||||
className="text-base font-bold mb-2"
|
||||
style={{ color: trader.ai_model === 'qwen' ? '#c084fc' : '#60a5fa' }}
|
||||
>
|
||||
{trader.trader_name}
|
||||
</div>
|
||||
<div className="text-2xl font-bold mono mb-1" style={{ color: trader.total_pnl >= 0 ? '#0ECB81' : '#F6465D' }}>
|
||||
{trader.total_pnl >= 0 ? '+' : ''}{trader.total_pnl_pct.toFixed(2)}%
|
||||
</div>
|
||||
{isWinning && gap > 0 && (
|
||||
<div className="text-xs font-semibold" style={{ color: '#0ECB81' }}>
|
||||
{t('leadingBy', language, { gap: gap.toFixed(2) })}
|
||||
</div>
|
||||
)}
|
||||
{!isWinning && gap < 0 && (
|
||||
<div className="text-xs font-semibold" style={{ color: '#F6465D' }}>
|
||||
{t('behindBy', language, { gap: Math.abs(gap).toFixed(2) })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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学习数据失败',
|
||||
|
||||
@@ -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<CompetitionData> {
|
||||
const res = await fetch(`${API_BASE}/competition`);
|
||||
if (!res.ok) throw new Error('获取竞赛数据失败');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
// AI交易员管理接口
|
||||
async getTraders(): Promise<TraderInfo[]> {
|
||||
const res = await fetch(`${API_BASE}/traders`);
|
||||
if (!res.ok) throw new Error('获取trader列表失败');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
async createTrader(request: CreateTraderRequest): Promise<TraderInfo> {
|
||||
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<void> {
|
||||
const res = await fetch(`${API_BASE}/traders/${traderId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!res.ok) throw new Error('删除交易员失败');
|
||||
},
|
||||
|
||||
async startTrader(traderId: string): Promise<void> {
|
||||
const res = await fetch(`${API_BASE}/traders/${traderId}/start`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!res.ok) throw new Error('启动交易员失败');
|
||||
},
|
||||
|
||||
async stopTrader(traderId: string): Promise<void> {
|
||||
const res = await fetch(`${API_BASE}/traders/${traderId}/stop`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!res.ok) throw new Error('停止交易员失败');
|
||||
},
|
||||
|
||||
// AI模型配置接口
|
||||
async getModelConfigs(): Promise<AIModel[]> {
|
||||
const res = await fetch(`${API_BASE}/models`);
|
||||
if (!res.ok) throw new Error('获取模型配置失败');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
async updateModelConfigs(request: UpdateModelConfigRequest): Promise<void> {
|
||||
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<Exchange[]> {
|
||||
const res = await fetch(`${API_BASE}/exchanges`);
|
||||
if (!res.ok) throw new Error('获取交易所配置失败');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
async updateExchangeConfigs(request: UpdateExchangeConfigRequest): Promise<void> {
|
||||
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<SystemStatus> {
|
||||
const url = traderId
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export default defineConfig({
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
target: 'http://localhost:8081',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user