diff --git a/.env.example b/.env.example index da0512fa..cd64fe4e 100644 --- a/.env.example +++ b/.env.example @@ -1,21 +1,6 @@ # NOFX Environment Variables Template # Copy this file to .env and modify the values as needed -# PostgreSQL数据库配置 -POSTGRES_HOST=postgres -POSTGRES_PORT=5432 -POSTGRES_DB=nofx -POSTGRES_USER=nofx -POSTGRES_PASSWORD=nofx123456 - -# Redis配置 -REDIS_HOST=redis -REDIS_PORT=6379 -REDIS_PASSWORD=redis123456 - -# 数据加密密钥 -DATA_ENCRYPTION_KEY=my_secret_encryption_key - # Ports Configuration # Backend API server port (internal: 8080, external: configurable) NOFX_BACKEND_PORT=8080 diff --git a/api/server.go b/api/server.go index e2b06f1c..b3f67f11 100644 --- a/api/server.go +++ b/api/server.go @@ -17,6 +17,7 @@ import ( "strconv" "strings" "time" + "github.com/gin-gonic/gin" "github.com/google/uuid" ) diff --git a/config.json.example b/config.json.example index ccdccca1..1c7406d0 100644 --- a/config.json.example +++ b/config.json.example @@ -22,20 +22,5 @@ "jwt_secret": "Qk0kAa+d0iIEzXVHXbNbm+UaN3RNabmWtH8rDWZ5OPf+4GX8pBflAHodfpbipVMyrw1fsDanHsNBjhgbDeK9Jg==", "log": { "level": "info" - }, - "proxy": { - "enabled": false, - "mode": "single", - "timeout": 30, - "proxy_url": "http://127.0.0.1:7890", - "proxy_list": [], - "brightdata_endpoint": "", - "brightdata_token": "", - "brightdata_zone": "", - "proxy_host": "", - "proxy_user": "", - "proxy_password": "", - "refresh_interval": 0, - "blacklist_ttl": 5 } -} +} \ No newline at end of file diff --git a/config/database_pg.go b/config/database_pg.go deleted file mode 100644 index 1acee98f..00000000 --- a/config/database_pg.go +++ /dev/null @@ -1,1046 +0,0 @@ -package config - -import ( - "database/sql" - "encoding/json" - "fmt" - "log" - "nofx/crypto" - "nofx/market" - "os" - "slices" - "strings" - "time" - - _ "github.com/lib/pq" -) - -// PostgreSQLDatabase PostgreSQL数据库配置 -type PostgreSQLDatabase struct { - db *sql.DB - cryptoService *crypto.CryptoService -} - -// NewPostgreSQLDatabase 创建PostgreSQL数据库连接 -func NewPostgreSQLDatabase() (*PostgreSQLDatabase, error) { - // 从环境变量获取数据库连接信息 - host := getEnv("POSTGRES_HOST", "localhost") - port := getEnv("POSTGRES_PORT", "5432") - dbname := getEnv("POSTGRES_DB", "nofx") - user := getEnv("POSTGRES_USER", "nofx") - password := getEnv("POSTGRES_PASSWORD", "nofx123456") - - // 构建连接字符串 - dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", - host, port, user, password, dbname) - - log.Printf("📋 连接PostgreSQL数据库: %s:%s/%s", host, port, dbname) - - db, err := sql.Open("postgres", dsn) - if err != nil { - return nil, fmt.Errorf("打开PostgreSQL数据库失败: %w", err) - } - - // 测试连接 - if err := db.Ping(); err != nil { - return nil, fmt.Errorf("连接PostgreSQL数据库失败: %w", err) - } - - // 设置连接池参数 - db.SetMaxOpenConns(25) - db.SetMaxIdleConns(5) - db.SetConnMaxLifetime(time.Hour) - - database := &PostgreSQLDatabase{db: db} - log.Printf("✅ PostgreSQL数据库连接成功") - - // 初始化默认数据 - if err := database.initDefaultData(); err != nil { - return nil, fmt.Errorf("初始化默认数据失败: %w", err) - } - - return database, nil -} - -func (d *PostgreSQLDatabase) SetCryptoService(cs *crypto.CryptoService) { - d.cryptoService = cs -} - -func (d *PostgreSQLDatabase) encryptValue(value string, aadParts ...string) (string, error) { - if value == "" { - return "", nil - } - if d.cryptoService == nil { - return "", fmt.Errorf("crypto service not initialized") - } - if !d.cryptoService.HasDataKey() { - return "", fmt.Errorf("data encryption key not configured") - } - if d.cryptoService.IsEncryptedStorageValue(value) { - return value, nil - } - return d.cryptoService.EncryptForStorage(value, aadParts...) -} - -func (d *PostgreSQLDatabase) decryptValue(value string, aadParts ...string) (string, error) { - if value == "" { - return "", nil - } - if d.cryptoService == nil { - return "", fmt.Errorf("crypto service not initialized") - } - if !d.cryptoService.HasDataKey() { - return "", fmt.Errorf("data encryption key not configured") - } - if !d.cryptoService.IsEncryptedStorageValue(value) { - return "", fmt.Errorf("value is not encrypted") - } - return d.cryptoService.DecryptFromStorage(value, aadParts...) -} - -// getEnv 获取环境变量,如果不存在返回默认值 -func getEnv(key, defaultValue string) string { - if value := os.Getenv(key); value != "" { - return value - } - return defaultValue -} - -// CreateUser 创建用户 -func (d *PostgreSQLDatabase) CreateUser(user *User) error { - _, err := d.db.Exec(` - INSERT INTO users (id, email, password_hash, otp_secret, otp_verified) - VALUES ($1, $2, $3, $4, $5) - `, user.ID, user.Email, user.PasswordHash, user.OTPSecret, user.OTPVerified) - return err -} - -// GetUserByEmail 通过邮箱获取用户 -func (d *PostgreSQLDatabase) GetUserByEmail(email string) (*User, error) { - var user User - err := d.db.QueryRow(` - SELECT id, email, password_hash, otp_secret, otp_verified, created_at, updated_at - FROM users WHERE email = $1 - `, email).Scan( - &user.ID, &user.Email, &user.PasswordHash, &user.OTPSecret, - &user.OTPVerified, &user.CreatedAt, &user.UpdatedAt, - ) - if err != nil { - return nil, err - } - return &user, nil -} - -// GetUserByID 通过ID获取用户 -func (d *PostgreSQLDatabase) GetUserByID(userID string) (*User, error) { - var user User - err := d.db.QueryRow(` - SELECT id, email, password_hash, otp_secret, otp_verified, created_at, updated_at - FROM users WHERE id = $1 - `, userID).Scan( - &user.ID, &user.Email, &user.PasswordHash, &user.OTPSecret, - &user.OTPVerified, &user.CreatedAt, &user.UpdatedAt, - ) - if err != nil { - return nil, err - } - return &user, nil -} - -// GetAllUsers 获取所有用户ID列表 -func (d *PostgreSQLDatabase) GetAllUsers() ([]string, error) { - rows, err := d.db.Query(`SELECT id FROM users ORDER BY id`) - if err != nil { - return nil, err - } - defer rows.Close() - - var userIDs []string - for rows.Next() { - var userID string - if err := rows.Scan(&userID); err != nil { - return nil, err - } - userIDs = append(userIDs, userID) - } - return userIDs, nil -} - -// UpdateUserOTPVerified 更新用户OTP验证状态 -func (d *PostgreSQLDatabase) UpdateUserOTPVerified(userID string, verified bool) error { - _, err := d.db.Exec(`UPDATE users SET otp_verified = $1 WHERE id = $2`, verified, userID) - return err -} - -// GetAIModels 获取用户的AI模型配置 -func (d *PostgreSQLDatabase) GetAIModels(userID string) ([]*AIModelConfig, error) { - rows, err := d.db.Query(` - SELECT id, user_id, name, provider, enabled, api_key, - COALESCE(custom_api_url, '') as custom_api_url, - COALESCE(custom_model_name, '') as custom_model_name, - COALESCE(deleted, FALSE) as deleted, - created_at, updated_at - FROM ai_models WHERE user_id = $1 AND COALESCE(deleted, FALSE) = FALSE ORDER BY id - `, userID) - if err != nil { - return nil, err - } - defer rows.Close() - - // 初始化为空切片而不是nil,确保JSON序列化为[]而不是null - models := make([]*AIModelConfig, 0) - for rows.Next() { - var model AIModelConfig - var deleted bool // 临时变量,用于读取 deleted 字段但不保存到结构体 - err := rows.Scan( - &model.ID, &model.UserID, &model.Name, &model.Provider, - &model.Enabled, &model.APIKey, &model.CustomAPIURL, &model.CustomModelName, - &deleted, &model.CreatedAt, &model.UpdatedAt, - ) - if err != nil { - return nil, err - } - - if model.APIKey != "" { - decrypted, err := d.decryptValue(model.APIKey, model.UserID, model.ID, "api_key") - if err != nil { - return nil, err - } - model.APIKey = decrypted - } - - models = append(models, &model) - } - - return models, nil -} - -// UpdateAIModel 更新AI模型配置,如果不存在则创建用户特定配置 -func (d *PostgreSQLDatabase) UpdateAIModel(userID, id string, enabled bool, apiKey, customAPIURL, customModelName string) error { - log.Printf("🔧 UpdateAIModel: userID=%s, id=%s, enabled=%v", userID, id, enabled) - - // 检查是否为删除操作(API Key 为空且 enabled 为 false 表示删除) - isDelete := !enabled && apiKey == "" && customAPIURL == "" && customModelName == "" - - if isDelete { - // 执行软删除:标记为已删除并清空敏感数据 - // 先尝试精确匹配 ID - var existingID string - err := d.db.QueryRow(` - SELECT id FROM ai_models WHERE user_id = $1 AND id = $2 LIMIT 1 - `, userID, id).Scan(&existingID) - - if err == nil { - // 找到了现有配置(精确匹配 ID),标记为删除并清空敏感数据 - _, err = d.db.Exec(` - UPDATE ai_models SET enabled = FALSE, deleted = TRUE, api_key = '', custom_api_url = '', custom_model_name = '', updated_at = CURRENT_TIMESTAMP - WHERE id = $1 AND user_id = $2 - `, existingID, userID) - if err != nil { - log.Printf("❌ UpdateAIModel: 标记删除失败: %v", err) - return err - } - log.Printf("🗑️ UpdateAIModel: 已标记删除用户 %s 的模型配置 %s", userID, existingID) - return nil - } - - // ID 不存在,尝试兼容旧逻辑:将 id 作为 provider 查找 - provider := id - err = d.db.QueryRow(` - SELECT id FROM ai_models WHERE user_id = $1 AND provider = $2 LIMIT 1 - `, userID, provider).Scan(&existingID) - - if err == nil { - // 找到了现有配置(通过 provider 匹配),标记为删除并清空敏感数据 - _, err = d.db.Exec(` - UPDATE ai_models SET enabled = FALSE, deleted = TRUE, api_key = '', custom_api_url = '', custom_model_name = '', updated_at = CURRENT_TIMESTAMP - WHERE id = $1 AND user_id = $2 - `, existingID, userID) - if err != nil { - log.Printf("❌ UpdateAIModel: 标记删除失败: %v", err) - return err - } - log.Printf("🗑️ UpdateAIModel: 已标记删除用户 %s 的模型配置 %s (通过provider匹配)", userID, existingID) - return nil - } - - // 没有找到配置,返回成功(幂等性) - log.Printf("ℹ️ UpdateAIModel: 模型配置不存在,跳过删除: %s", id) - return nil - } - - // 启用模型的情况:先尝试精确匹配 ID(新版逻辑,支持多个相同 provider 的模型) - var existingID string - err := d.db.QueryRow(` - SELECT id FROM ai_models WHERE user_id = $1 AND id = $2 LIMIT 1 - `, userID, id).Scan(&existingID) - - if err == nil { - apiKeyEnc, err := d.encryptValue(apiKey, userID, existingID, "api_key") - if err != nil { - return err - } - // 找到了现有配置(精确匹配 ID),更新它 - _, err = d.db.Exec(` - UPDATE ai_models SET enabled = $1, api_key = $2, custom_api_url = $3, custom_model_name = $4, deleted = FALSE, updated_at = CURRENT_TIMESTAMP - WHERE id = $5 AND user_id = $6 - `, enabled, apiKeyEnc, customAPIURL, customModelName, existingID, userID) - return err - } - if err != sql.ErrNoRows { - return err - } - - // ID 不存在,尝试兼容旧逻辑:将 id 作为 provider 查找 - provider := id - err = d.db.QueryRow(` - SELECT id FROM ai_models WHERE user_id = $1 AND provider = $2 LIMIT 1 - `, userID, provider).Scan(&existingID) - - if err == nil { - apiKeyEnc, err := d.encryptValue(apiKey, userID, existingID, "api_key") - if err != nil { - return err - } - // 找到了现有配置(通过 provider 匹配,兼容旧版),更新它 - log.Printf("⚠️ 使用旧版 provider 匹配更新模型: %s -> %s", provider, existingID) - _, err = d.db.Exec(` - UPDATE ai_models SET enabled = $1, api_key = $2, custom_api_url = $3, custom_model_name = $4, deleted = FALSE, updated_at = CURRENT_TIMESTAMP - WHERE id = $5 AND user_id = $6 - `, enabled, apiKeyEnc, customAPIURL, customModelName, existingID, userID) - return err - } - if err != sql.ErrNoRows { - return err - } - - // 没有找到任何现有配置,创建新的 - // 推断 provider(从 id 中提取,或者直接使用 id) - if provider == id && (provider == "deepseek" || provider == "qwen") { - // id 本身就是 provider - provider = id - } else { - // 从 id 中提取 provider(假设格式是 userID_provider 或 timestamp_userID_provider) - parts := strings.Split(id, "_") - if len(parts) >= 2 { - provider = parts[len(parts)-1] // 取最后一部分作为 provider - } else { - provider = id - } - } - - // 获取模型的基本信息 - var name string - err = d.db.QueryRow(` - SELECT name FROM ai_models WHERE provider = $1 LIMIT 1 - `, provider).Scan(&name) - if err != nil { - // 如果找不到基本信息,使用默认值 - if provider == "deepseek" { - name = "DeepSeek AI" - } else if provider == "qwen" { - name = "Qwen AI" - } else { - name = provider + " AI" - } - } - - // 如果传入的 ID 已经是完整格式(如 "admin_deepseek_custom1"),直接使用 - // 否则生成新的 ID - newModelID := id - if id == provider { - // id 就是 provider,生成新的用户特定 ID - newModelID = fmt.Sprintf("%s_%s", userID, provider) - } - - apiKeyEnc, err := d.encryptValue(apiKey, userID, newModelID, "api_key") - if err != nil { - return err - } - - log.Printf("✓ 创建新的 AI 模型配置: ID=%s, Provider=%s, Name=%s", newModelID, provider, name) - _, err = d.db.Exec(` - INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, custom_model_name, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) - `, newModelID, userID, name, provider, enabled, apiKeyEnc, customAPIURL, customModelName) - - return err -} - -// GetExchanges 获取用户的交易所配置 -func (d *PostgreSQLDatabase) GetExchanges(userID string) ([]*ExchangeConfig, error) { - rows, err := d.db.Query(` - SELECT id, user_id, name, type, enabled, api_key, secret_key, testnet, - COALESCE(hyperliquid_wallet_addr, '') AS hyperliquid_wallet_addr, - COALESCE(aster_user, '') AS aster_user, - COALESCE(aster_signer, '') AS aster_signer, - COALESCE(aster_private_key, '') AS aster_private_key, - COALESCE(dex_wallet_private_key, '') AS dex_wallet_private_key, - COALESCE(deleted, FALSE) AS deleted, - created_at, updated_at - FROM exchanges - WHERE user_id = $1 AND COALESCE(deleted, FALSE) = FALSE - ORDER BY id - `, userID) - if err != nil { - return nil, err - } - defer rows.Close() - - // 初始化为空切片而不是nil,确保JSON序列化为[]而不是null - exchanges := make([]*ExchangeConfig, 0) - for rows.Next() { - var exchange ExchangeConfig - err := rows.Scan( - &exchange.ID, &exchange.UserID, &exchange.Name, &exchange.Type, - &exchange.Enabled, &exchange.APIKey, &exchange.SecretKey, &exchange.Testnet, - &exchange.HyperliquidWalletAddr, &exchange.AsterUser, - &exchange.AsterSigner, &exchange.AsterPrivateKey, - &exchange.DEXWalletPrivateKey, - &exchange.Deleted, - &exchange.CreatedAt, &exchange.UpdatedAt, - ) - if err != nil { - return nil, err - } - - if decrypted, err := d.decryptValue(exchange.APIKey, exchange.UserID, exchange.ID, "api_key"); err == nil { - exchange.APIKey = decrypted - } else { - return nil, err - } - if decrypted, err := d.decryptValue(exchange.SecretKey, exchange.UserID, exchange.ID, "secret_key"); err == nil { - exchange.SecretKey = decrypted - } else { - return nil, err - } - if decrypted, err := d.decryptValue(exchange.HyperliquidWalletAddr, exchange.UserID, exchange.ID, "hyperliquid_wallet_addr"); err == nil { - exchange.HyperliquidWalletAddr = decrypted - } else { - return nil, err - } - if decrypted, err := d.decryptValue(exchange.AsterUser, exchange.UserID, exchange.ID, "aster_user"); err == nil { - exchange.AsterUser = decrypted - } else { - return nil, err - } - if decrypted, err := d.decryptValue(exchange.AsterSigner, exchange.UserID, exchange.ID, "aster_signer"); err == nil { - exchange.AsterSigner = decrypted - } else { - return nil, err - } - if decrypted, err := d.decryptValue(exchange.AsterPrivateKey, exchange.UserID, exchange.ID, "aster_private_key"); err == nil { - exchange.AsterPrivateKey = decrypted - } else { - return nil, err - } - if decrypted, err := d.decryptValue(exchange.DEXWalletPrivateKey, exchange.UserID, exchange.ID, "dex_wallet_private_key"); err == nil { - exchange.DEXWalletPrivateKey = decrypted - } else { - return nil, err - } - - exchanges = append(exchanges, &exchange) - } - - return exchanges, nil -} - -// UpdateExchange 更新交易所配置,如果不存在则创建用户特定配置 -func (d *PostgreSQLDatabase) UpdateExchange(userID, id string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error { - log.Printf("🔧 UpdateExchange: userID=%s, id=%s, enabled=%v", userID, id, enabled) - - // 如果请求禁用该交易所,执行软删除 - if !enabled { - _, err := d.db.Exec(` - UPDATE exchanges - SET enabled = FALSE, - deleted = TRUE, - api_key = '', - secret_key = '', - testnet = FALSE, - hyperliquid_wallet_addr = '', - aster_user = '', - aster_signer = '', - aster_private_key = '', - updated_at = CURRENT_TIMESTAMP - WHERE id = $1 AND user_id = $2 - `, id, userID) - if err != nil { - log.Printf("❌ UpdateExchange: 标记删除失败: %v", err) - return err - } - log.Printf("🗑️ UpdateExchange: 已标记删除用户 %s 的交易所配置 %s", userID, id) - return nil - } - - apiKeyEnc, err := d.encryptValue(apiKey, userID, id, "api_key") - if err != nil { - return fmt.Errorf("encrypt api_key failed: %w", err) - } - secretKeyEnc, err := d.encryptValue(secretKey, userID, id, "secret_key") - if err != nil { - return fmt.Errorf("encrypt secret_key failed: %w", err) - } - hyperAddrEnc, err := d.encryptValue(hyperliquidWalletAddr, userID, id, "hyperliquid_wallet_addr") - if err != nil { - return fmt.Errorf("encrypt hyperliquid_wallet_addr failed: %w", err) - } - asterUserEnc, err := d.encryptValue(asterUser, userID, id, "aster_user") - if err != nil { - return fmt.Errorf("encrypt aster_user failed: %w", err) - } - asterSignerEnc, err := d.encryptValue(asterSigner, userID, id, "aster_signer") - if err != nil { - return fmt.Errorf("encrypt aster_signer failed: %w", err) - } - asterPrivateKeyEnc, err := d.encryptValue(asterPrivateKey, userID, id, "aster_private_key") - if err != nil { - return fmt.Errorf("encrypt aster_private_key failed: %w", err) - } - - // 首先尝试更新现有的用户配置 - result, err := d.db.Exec(` - UPDATE exchanges SET enabled = $1, api_key = $2, secret_key = $3, testnet = $4, - hyperliquid_wallet_addr = $5, aster_user = $6, aster_signer = $7, aster_private_key = $8, - deleted = FALSE, updated_at = CURRENT_TIMESTAMP - WHERE id = $9 AND user_id = $10 - `, enabled, apiKeyEnc, secretKeyEnc, testnet, hyperAddrEnc, asterUserEnc, asterSignerEnc, asterPrivateKeyEnc, id, userID) - if err != nil { - log.Printf("❌ UpdateExchange: 更新失败: %v", err) - return err - } - - // 检查是否有行被更新 - rowsAffected, err := result.RowsAffected() - if err != nil { - log.Printf("❌ UpdateExchange: 获取影响行数失败: %v", err) - return err - } - - log.Printf("📊 UpdateExchange: 影响行数 = %d", rowsAffected) - - // 如果没有行被更新,说明用户没有这个交易所的配置,需要创建 - if rowsAffected == 0 { - log.Printf("💡 UpdateExchange: 没有现有记录,创建新记录") - - // 根据交易所ID确定基本信息 - var name, typ string - if id == "binance" { - name = "Binance Futures" - typ = "cex" - } else if id == "hyperliquid" { - name = "Hyperliquid" - typ = "dex" - } else if id == "aster" { - name = "Aster DEX" - typ = "dex" - } else { - name = id + " Exchange" - typ = "cex" - } - - log.Printf("🆕 UpdateExchange: 创建新记录 ID=%s, name=%s, type=%s", id, name, typ) - - // 创建用户特定的配置,使用原始的交易所ID - _, err = d.db.Exec(` - INSERT INTO exchanges (id, user_id, name, type, enabled, api_key, secret_key, testnet, - hyperliquid_wallet_addr, aster_user, aster_signer, aster_private_key, - deleted, created_at, updated_at) - VALUES ($1, $2, $3, $4, TRUE, $5, $6, $7, $8, $9, $10, $11, FALSE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) - `, id, userID, name, typ, apiKeyEnc, secretKeyEnc, testnet, hyperAddrEnc, asterUserEnc, asterSignerEnc, asterPrivateKeyEnc) - - if err != nil { - log.Printf("❌ UpdateExchange: 创建记录失败: %v", err) - } else { - log.Printf("✅ UpdateExchange: 创建记录成功") - } - return err - } - - log.Printf("✅ UpdateExchange: 更新现有记录成功") - return nil -} - -// CreateAIModel 创建AI模型配置 -func (d *PostgreSQLDatabase) CreateAIModel(userID, id, name, provider string, enabled bool, apiKey, customAPIURL string) error { - apiKeyEnc, err := d.encryptValue(apiKey, userID, id, "api_key") - if err != nil { - return err - } - - _, err = d.db.Exec(` - INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url) - VALUES ($1, $2, $3, $4, $5, $6, $7) - ON CONFLICT (id) DO NOTHING - `, id, userID, name, provider, enabled, apiKeyEnc, customAPIURL) - return err -} - -// CreateExchange 创建交易所配置 -func (d *PostgreSQLDatabase) CreateExchange(userID, id, name, typ string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error { - apiKeyEnc, err := d.encryptValue(apiKey, userID, id, "api_key") - if err != nil { - return fmt.Errorf("encrypt api_key failed: %w", err) - } - secretKeyEnc, err := d.encryptValue(secretKey, userID, id, "secret_key") - if err != nil { - return fmt.Errorf("encrypt secret_key failed: %w", err) - } - hyperAddrEnc, err := d.encryptValue(hyperliquidWalletAddr, userID, id, "hyperliquid_wallet_addr") - if err != nil { - return fmt.Errorf("encrypt hyperliquid_wallet_addr failed: %w", err) - } - asterUserEnc, err := d.encryptValue(asterUser, userID, id, "aster_user") - if err != nil { - return fmt.Errorf("encrypt aster_user failed: %w", err) - } - asterSignerEnc, err := d.encryptValue(asterSigner, userID, id, "aster_signer") - if err != nil { - return fmt.Errorf("encrypt aster_signer failed: %w", err) - } - asterPrivateKeyEnc, err := d.encryptValue(asterPrivateKey, userID, id, "aster_private_key") - if err != nil { - return fmt.Errorf("encrypt aster_private_key failed: %w", err) - } - - _, err = d.db.Exec(` - INSERT INTO exchanges (id, user_id, name, type, enabled, api_key, secret_key, testnet, hyperliquid_wallet_addr, aster_user, aster_signer, aster_private_key) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) - ON CONFLICT (id, user_id) DO NOTHING - `, id, userID, name, typ, enabled, apiKeyEnc, secretKeyEnc, testnet, hyperAddrEnc, asterUserEnc, asterSignerEnc, asterPrivateKeyEnc) - return err -} - -// CreateTrader 创建交易员 -func (d *PostgreSQLDatabase) CreateTrader(trader *TraderRecord) error { - _, err := d.db.Exec(` - INSERT INTO traders (id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running, btc_eth_leverage, altcoin_leverage, trading_symbols, use_coin_pool, use_oi_top, custom_prompt, override_base_prompt, system_prompt_template, is_cross_margin) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) - `, trader.ID, trader.UserID, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance, trader.ScanIntervalMinutes, trader.IsRunning, trader.BTCETHLeverage, trader.AltcoinLeverage, trader.TradingSymbols, trader.UseCoinPool, trader.UseOITop, trader.CustomPrompt, trader.OverrideBasePrompt, trader.SystemPromptTemplate, trader.IsCrossMargin) - return err -} - -// GetTraders 获取用户的交易员 -func (d *PostgreSQLDatabase) GetTraders(userID string) ([]*TraderRecord, error) { - rows, err := d.db.Query(` - SELECT id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running, - COALESCE(btc_eth_leverage, 5) as btc_eth_leverage, COALESCE(altcoin_leverage, 5) as altcoin_leverage, - COALESCE(trading_symbols, '') as trading_symbols, - COALESCE(use_coin_pool, false) as use_coin_pool, COALESCE(use_oi_top, false) as use_oi_top, - COALESCE(custom_prompt, '') as custom_prompt, COALESCE(override_base_prompt, false) as override_base_prompt, - COALESCE(system_prompt_template, 'default') as system_prompt_template, - COALESCE(is_cross_margin, true) as is_cross_margin, created_at, updated_at - FROM traders WHERE user_id = $1 ORDER BY created_at DESC - `, userID) - if err != nil { - return nil, err - } - defer rows.Close() - - var traders []*TraderRecord - for rows.Next() { - var trader TraderRecord - err := rows.Scan( - &trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID, - &trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning, - &trader.BTCETHLeverage, &trader.AltcoinLeverage, &trader.TradingSymbols, - &trader.UseCoinPool, &trader.UseOITop, - &trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.SystemPromptTemplate, - &trader.IsCrossMargin, - &trader.CreatedAt, &trader.UpdatedAt, - ) - if err != nil { - return nil, err - } - traders = append(traders, &trader) - } - - return traders, nil -} - -// UpdateTraderStatus 更新交易员状态 -func (d *PostgreSQLDatabase) UpdateTraderStatus(userID, id string, isRunning bool) error { - _, err := d.db.Exec(`UPDATE traders SET is_running = $1 WHERE id = $2 AND user_id = $3`, isRunning, id, userID) - return err -} - -// UpdateTrader 更新交易员配置 -func (d *PostgreSQLDatabase) UpdateTrader(trader *TraderRecord) error { - _, err := d.db.Exec(` - UPDATE traders SET - name = $1, ai_model_id = $2, exchange_id = $3, initial_balance = $4, - scan_interval_minutes = $5, btc_eth_leverage = $6, altcoin_leverage = $7, - trading_symbols = $8, custom_prompt = $9, override_base_prompt = $10, - system_prompt_template = $11, is_cross_margin = $12, updated_at = CURRENT_TIMESTAMP - WHERE id = $13 AND user_id = $14 - `, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance, - trader.ScanIntervalMinutes, trader.BTCETHLeverage, trader.AltcoinLeverage, - trader.TradingSymbols, trader.CustomPrompt, trader.OverrideBasePrompt, - trader.SystemPromptTemplate, trader.IsCrossMargin, trader.ID, trader.UserID) - return err -} - -// UpdateTraderCustomPrompt 更新交易员自定义Prompt -func (d *PostgreSQLDatabase) UpdateTraderCustomPrompt(userID, id string, customPrompt string, overrideBase bool) error { - _, err := d.db.Exec(`UPDATE traders SET custom_prompt = $1, override_base_prompt = $2 WHERE id = $3 AND user_id = $4`, customPrompt, overrideBase, id, userID) - return err -} - -// UpdateTraderInitialBalance 更新交易员初始余额(用于自动同步交易所实际余额) -func (d *PostgreSQLDatabase) UpdateTraderInitialBalance(userID, id string, newBalance float64) error { - _, err := d.db.Exec(`UPDATE traders SET initial_balance = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 AND user_id = $3`, newBalance, id, userID) - return err -} - -// DeleteTrader 删除交易员 -func (d *PostgreSQLDatabase) DeleteTrader(userID, id string) error { - _, err := d.db.Exec(`DELETE FROM traders WHERE id = $1 AND user_id = $2`, id, userID) - return err -} - -// GetTraderConfig 获取交易员完整配置(包含AI模型和交易所信息) -func (d *PostgreSQLDatabase) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIModelConfig, *ExchangeConfig, error) { - var trader TraderRecord - var aiModel AIModelConfig - var exchange ExchangeConfig - - err := d.db.QueryRow(` - SELECT - t.id, t.user_id, t.name, t.ai_model_id, t.exchange_id, t.initial_balance, t.scan_interval_minutes, t.is_running, t.created_at, t.updated_at, - a.id, a.user_id, a.name, a.provider, a.enabled, a.api_key, a.created_at, a.updated_at, - e.id, e.user_id, e.name, e.type, e.enabled, e.api_key, e.secret_key, e.testnet, - COALESCE(e.hyperliquid_wallet_addr, '') as hyperliquid_wallet_addr, - COALESCE(e.aster_user, '') as aster_user, - COALESCE(e.aster_signer, '') as aster_signer, - COALESCE(e.aster_private_key, '') as aster_private_key, - e.created_at, e.updated_at - FROM traders t - JOIN ai_models a ON t.ai_model_id = a.id AND t.user_id = a.user_id - JOIN exchanges e ON t.exchange_id = e.id AND t.user_id = e.user_id - WHERE t.id = $1 AND t.user_id = $2 - `, traderID, userID).Scan( - &trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID, - &trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning, - &trader.CreatedAt, &trader.UpdatedAt, - &aiModel.ID, &aiModel.UserID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey, - &aiModel.CreatedAt, &aiModel.UpdatedAt, - &exchange.ID, &exchange.UserID, &exchange.Name, &exchange.Type, &exchange.Enabled, - &exchange.APIKey, &exchange.SecretKey, &exchange.Testnet, - &exchange.HyperliquidWalletAddr, &exchange.AsterUser, &exchange.AsterSigner, &exchange.AsterPrivateKey, - &exchange.CreatedAt, &exchange.UpdatedAt, - ) - - if err != nil { - return nil, nil, nil, err - } - - if aiModel.APIKey != "" { - decrypted, err := d.decryptValue(aiModel.APIKey, aiModel.UserID, aiModel.ID, "api_key") - if err != nil { - return nil, nil, nil, err - } - aiModel.APIKey = decrypted - } - - if exchange.APIKey != "" { - decrypted, err := d.decryptValue(exchange.APIKey, exchange.UserID, exchange.ID, "api_key") - if err != nil { - return nil, nil, nil, err - } - exchange.APIKey = decrypted - } - if exchange.SecretKey != "" { - decrypted, err := d.decryptValue(exchange.SecretKey, exchange.UserID, exchange.ID, "secret_key") - if err != nil { - return nil, nil, nil, err - } - exchange.SecretKey = decrypted - } - if exchange.HyperliquidWalletAddr != "" { - decrypted, err := d.decryptValue(exchange.HyperliquidWalletAddr, exchange.UserID, exchange.ID, "hyperliquid_wallet_addr") - if err != nil { - return nil, nil, nil, err - } - exchange.HyperliquidWalletAddr = decrypted - } - if exchange.AsterUser != "" { - decrypted, err := d.decryptValue(exchange.AsterUser, exchange.UserID, exchange.ID, "aster_user") - if err != nil { - return nil, nil, nil, err - } - exchange.AsterUser = decrypted - } - if exchange.AsterSigner != "" { - decrypted, err := d.decryptValue(exchange.AsterSigner, exchange.UserID, exchange.ID, "aster_signer") - if err != nil { - return nil, nil, nil, err - } - exchange.AsterSigner = decrypted - } - if exchange.AsterPrivateKey != "" { - decrypted, err := d.decryptValue(exchange.AsterPrivateKey, exchange.UserID, exchange.ID, "aster_private_key") - if err != nil { - return nil, nil, nil, err - } - exchange.AsterPrivateKey = decrypted - } - - return &trader, &aiModel, &exchange, nil -} - -// GetSystemConfig 获取系统配置 -func (d *PostgreSQLDatabase) GetSystemConfig(key string) (string, error) { - var value string - err := d.db.QueryRow(`SELECT value FROM system_config WHERE key = $1`, key).Scan(&value) - return value, err -} - -// SetSystemConfig 设置系统配置 -func (d *PostgreSQLDatabase) SetSystemConfig(key, value string) error { - _, err := d.db.Exec(` - INSERT INTO system_config (key, value) VALUES ($1, $2) - ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = CURRENT_TIMESTAMP - `, key, value) - return err -} - -// CreateUserSignalSource 创建用户信号源配置 -func (d *PostgreSQLDatabase) CreateUserSignalSource(userID, coinPoolURL, oiTopURL string) error { - _, err := d.db.Exec(` - INSERT INTO user_signal_sources (user_id, coin_pool_url, oi_top_url, updated_at) - VALUES ($1, $2, $3, CURRENT_TIMESTAMP) - ON CONFLICT (user_id) DO UPDATE SET - coin_pool_url = $2, oi_top_url = $3, updated_at = CURRENT_TIMESTAMP - `, userID, coinPoolURL, oiTopURL) - return err -} - -// GetUserSignalSource 获取用户信号源配置 -func (d *PostgreSQLDatabase) GetUserSignalSource(userID string) (*UserSignalSource, error) { - var source UserSignalSource - err := d.db.QueryRow(` - SELECT id, user_id, coin_pool_url, oi_top_url, created_at, updated_at - FROM user_signal_sources WHERE user_id = $1 - `, userID).Scan( - &source.ID, &source.UserID, &source.CoinPoolURL, &source.OITopURL, - &source.CreatedAt, &source.UpdatedAt, - ) - if err != nil { - return nil, err - } - return &source, nil -} - -// UpdateUserSignalSource 更新用户信号源配置 -func (d *PostgreSQLDatabase) UpdateUserSignalSource(userID, coinPoolURL, oiTopURL string) error { - _, err := d.db.Exec(` - UPDATE user_signal_sources SET coin_pool_url = $1, oi_top_url = $2, updated_at = CURRENT_TIMESTAMP - WHERE user_id = $3 - `, coinPoolURL, oiTopURL, userID) - return err -} - -// GetCustomCoins 获取所有交易员自定义币种 -func (d *PostgreSQLDatabase) GetCustomCoins() []string { - var symbol string - var symbols []string - - err := d.db.QueryRow(` - SELECT STRING_AGG(custom_coins, ',') as symbol - FROM traders WHERE custom_coins != '' - `).Scan(&symbol) - - // 检测用户是否未配置币种 - 兼容性 - if err != nil || symbol == "" { - symbolJSON, _ := d.GetSystemConfig("default_coins") - if err := json.Unmarshal([]byte(symbolJSON), &symbols); err != nil { - log.Printf("⚠️ 解析default_coins配置失败: %v,使用硬编码默认值", err) - symbols = []string{"BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT"} - } - } - - // filter Symbol - for _, s := range strings.Split(symbol, ",") { - if s == "" { - continue - } - coin := market.Normalize(s) - if !slices.Contains(symbols, coin) { - symbols = append(symbols, coin) - } - } - return symbols -} - -// LoadBetaCodesFromFile 从文件加载内测码到数据库 -func (d *PostgreSQLDatabase) LoadBetaCodesFromFile(filePath string) error { - // 读取文件内容 - content, err := os.ReadFile(filePath) - if err != nil { - return fmt.Errorf("读取内测码文件失败: %w", err) - } - - // 按行分割内测码 - lines := strings.Split(string(content), "\n") - var codes []string - for _, line := range lines { - code := strings.TrimSpace(line) - if code != "" && !strings.HasPrefix(code, "#") { - codes = append(codes, code) - } - } - - // 批量插入内测码 - tx, err := d.db.Begin() - if err != nil { - return fmt.Errorf("开始事务失败: %w", err) - } - defer tx.Rollback() - - stmt, err := tx.Prepare(`INSERT INTO beta_codes (code) VALUES ($1) ON CONFLICT (code) DO NOTHING`) - if err != nil { - return fmt.Errorf("准备语句失败: %w", err) - } - defer stmt.Close() - - insertedCount := 0 - for _, code := range codes { - result, err := stmt.Exec(code) - if err != nil { - log.Printf("插入内测码 %s 失败: %v", code, err) - continue - } - - if rowsAffected, _ := result.RowsAffected(); rowsAffected > 0 { - insertedCount++ - } - } - - if err := tx.Commit(); err != nil { - return fmt.Errorf("提交事务失败: %w", err) - } - - log.Printf("✅ 成功加载 %d 个内测码到数据库 (总计 %d 个)", insertedCount, len(codes)) - return nil -} - -// ValidateBetaCode 验证内测码是否有效且未使用 -func (d *PostgreSQLDatabase) ValidateBetaCode(code string) (bool, error) { - var used bool - err := d.db.QueryRow(`SELECT used FROM beta_codes WHERE code = $1`, code).Scan(&used) - if err != nil { - if err == sql.ErrNoRows { - return false, nil // 内测码不存在 - } - return false, err - } - return !used, nil // 内测码存在且未使用 -} - -// UseBetaCode 使用内测码(标记为已使用) -func (d *PostgreSQLDatabase) UseBetaCode(code, userEmail string) error { - result, err := d.db.Exec(` - UPDATE beta_codes SET used = true, used_by = $1, used_at = CURRENT_TIMESTAMP - WHERE code = $2 AND used = false - `, userEmail, code) - if err != nil { - return err - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - - if rowsAffected == 0 { - return fmt.Errorf("内测码无效或已被使用") - } - - return nil -} - -// GetBetaCodeStats 获取内测码统计信息 -func (d *PostgreSQLDatabase) GetBetaCodeStats() (total, used int, err error) { - err = d.db.QueryRow(`SELECT COUNT(*) FROM beta_codes`).Scan(&total) - if err != nil { - return 0, 0, err - } - - err = d.db.QueryRow(`SELECT COUNT(*) FROM beta_codes WHERE used = true`).Scan(&used) - if err != nil { - return 0, 0, err - } - - return total, used, nil -} - -// initDefaultData 初始化默认数据(AI模型和交易所) -func (d *PostgreSQLDatabase) initDefaultData() error { - // 确保traders表存在custom_coins列,防止旧环境缺少字段 - if _, err := d.db.Exec(`ALTER TABLE traders ADD COLUMN IF NOT EXISTS custom_coins TEXT DEFAULT ''`); err != nil { - return fmt.Errorf("添加custom_coins列失败: %w", err) - } - - // 确保exchanges表存在deleted列 - if _, err := d.db.Exec(`ALTER TABLE exchanges ADD COLUMN IF NOT EXISTS deleted BOOLEAN DEFAULT FALSE`); err != nil { - return fmt.Errorf("添加deleted列失败: %w", err) - } - - // 首先创建default用户(如果不存在) - _, err := d.db.Exec(` - INSERT INTO users (id, email, password_hash, otp_secret, otp_verified) - VALUES ('default', 'default@localhost', '', '', true) - ON CONFLICT (id) DO NOTHING - `) - if err != nil { - return fmt.Errorf("创建default用户失败: %w", err) - } - - // 初始化AI模型(使用default用户) - aiModels := []struct { - id, name, provider string - }{ - {"deepseek", "DeepSeek", "deepseek"}, - {"qwen", "Qwen", "qwen"}, - } - - for _, model := range aiModels { - _, err := d.db.Exec(` - INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, custom_model_name, created_at, updated_at) - VALUES ($1, 'default', $2, $3, false, '', '', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) - ON CONFLICT (id) DO NOTHING - `, model.id, model.name, model.provider) - if err != nil { - return fmt.Errorf("初始化AI模型失败: %w", err) - } - } - - // 初始化交易所(使用default用户) - exchanges := []struct { - id, name, typ string - }{ - {"binance", "Binance Futures", "binance"}, - {"hyperliquid", "Hyperliquid", "hyperliquid"}, - {"aster", "Aster DEX", "aster"}, - } - - for _, exchange := range exchanges { - _, err := d.db.Exec(` - INSERT INTO exchanges (id, user_id, name, type, enabled, api_key, secret_key, testnet, - hyperliquid_wallet_addr, aster_user, aster_signer, aster_private_key, created_at, updated_at) - VALUES ($1, 'default', $2, $3, false, '', '', false, '', '', '', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) - ON CONFLICT (id, user_id) DO NOTHING - `, exchange.id, exchange.name, exchange.typ) - if err != nil { - return fmt.Errorf("初始化交易所失败: %w", err) - } - } - - return nil -} - -// Close 关闭数据库连接 -func (d *PostgreSQLDatabase) Close() error { - return d.db.Close() -} diff --git a/db/init.sql b/db/init.sql deleted file mode 100644 index c89fa78a..00000000 --- a/db/init.sql +++ /dev/null @@ -1,179 +0,0 @@ --- PostgreSQL初始化脚本 --- AI交易系统数据库迁移 - --- 用户表 -CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, - email TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - otp_secret TEXT, - otp_verified BOOLEAN DEFAULT FALSE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - --- AI模型配置表 -CREATE TABLE IF NOT EXISTS ai_models ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL DEFAULT 'default', - name TEXT NOT NULL, - provider TEXT NOT NULL, - enabled BOOLEAN DEFAULT FALSE, - api_key TEXT DEFAULT '', - custom_api_url TEXT DEFAULT '', - custom_model_name TEXT DEFAULT '', - deleted BOOLEAN DEFAULT FALSE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -); - --- 交易所配置表 -CREATE TABLE IF NOT EXISTS exchanges ( - id TEXT NOT NULL, - user_id TEXT NOT NULL DEFAULT 'default', - name TEXT NOT NULL, - type TEXT NOT NULL, -- 'cex' or 'dex' - enabled BOOLEAN DEFAULT FALSE, - api_key TEXT DEFAULT '', - secret_key TEXT DEFAULT '', - testnet BOOLEAN DEFAULT FALSE, - -- Hyperliquid 特定字段 - hyperliquid_wallet_addr TEXT DEFAULT '', - -- Aster 特定字段 - aster_user TEXT DEFAULT '', - aster_signer TEXT DEFAULT '', - aster_private_key TEXT DEFAULT '', - deleted BOOLEAN DEFAULT FALSE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id, user_id), - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -); - --- 用户信号源配置表 -CREATE TABLE IF NOT EXISTS user_signal_sources ( - id SERIAL PRIMARY KEY, - user_id TEXT NOT NULL, - coin_pool_url TEXT DEFAULT '', - oi_top_url TEXT DEFAULT '', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - UNIQUE(user_id) -); - --- 交易员配置表 -CREATE TABLE IF NOT EXISTS traders ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL DEFAULT 'default', - name TEXT NOT NULL, - ai_model_id TEXT NOT NULL, - exchange_id TEXT NOT NULL, - initial_balance REAL NOT NULL, - scan_interval_minutes INTEGER DEFAULT 3, - is_running BOOLEAN DEFAULT FALSE, - btc_eth_leverage INTEGER DEFAULT 5, - altcoin_leverage INTEGER DEFAULT 5, - trading_symbols TEXT DEFAULT '', - use_coin_pool BOOLEAN DEFAULT FALSE, - use_oi_top BOOLEAN DEFAULT FALSE, - custom_prompt TEXT DEFAULT '', - override_base_prompt BOOLEAN DEFAULT FALSE, - system_prompt_template TEXT DEFAULT 'default', - is_cross_margin BOOLEAN DEFAULT TRUE, - custom_coins TEXT DEFAULT '', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (ai_model_id) REFERENCES ai_models(id), - FOREIGN KEY (exchange_id, user_id) REFERENCES exchanges(id, user_id) -); - --- 系统配置表 -CREATE TABLE IF NOT EXISTS system_config ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - --- 内测码表 -CREATE TABLE IF NOT EXISTS beta_codes ( - code TEXT PRIMARY KEY, - used BOOLEAN DEFAULT FALSE, - used_by TEXT DEFAULT '', - used_at TIMESTAMP DEFAULT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - --- 自动更新 updated_at 函数 -CREATE OR REPLACE FUNCTION update_updated_at_column() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = CURRENT_TIMESTAMP; - RETURN NEW; -END; -$$ language 'plpgsql'; - --- 创建触发器:自动更新 updated_at -CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users - FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); - -CREATE TRIGGER update_ai_models_updated_at BEFORE UPDATE ON ai_models - FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); - -CREATE TRIGGER update_exchanges_updated_at BEFORE UPDATE ON exchanges - FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); - -CREATE TRIGGER update_traders_updated_at BEFORE UPDATE ON traders - FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); - -CREATE TRIGGER update_user_signal_sources_updated_at BEFORE UPDATE ON user_signal_sources - FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); - -CREATE TRIGGER update_system_config_updated_at BEFORE UPDATE ON system_config - FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); - --- 插入默认数据 - --- 创建default用户(如果不存在) -INSERT INTO users (id, email, password_hash, otp_secret, otp_verified) VALUES -('default', 'default@localhost', '', '', TRUE) -ON CONFLICT (id) DO NOTHING; - --- 初始化AI模型(使用default用户) -INSERT INTO ai_models (id, user_id, name, provider, enabled) VALUES -('deepseek', 'default', 'DeepSeek', 'deepseek', FALSE), -('qwen', 'default', 'Qwen', 'qwen', FALSE) -ON CONFLICT (id) DO NOTHING; - --- 初始化交易所(使用default用户) -INSERT INTO exchanges (id, user_id, name, type, enabled) VALUES -('binance', 'default', 'Binance Futures', 'binance', FALSE), -('hyperliquid', 'default', 'Hyperliquid', 'hyperliquid', FALSE), -('aster', 'default', 'Aster DEX', 'aster', FALSE) -ON CONFLICT (id, user_id) DO NOTHING; - --- 初始化系统配置 -INSERT INTO system_config (key, value) VALUES -('beta_mode', 'false'), -('api_server_port', '8080'), -('use_default_coins', 'true'), -('default_coins', '["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]'), -('max_daily_loss', '10.0'), -('max_drawdown', '20.0'), -('stop_trading_minutes', '60'), -('btc_eth_leverage', '5'), -('altcoin_leverage', '5'), -('jwt_secret', '') -ON CONFLICT (key) DO NOTHING; - --- 数据库迁移:添加 deleted 字段到现有 ai_models 表 -ALTER TABLE ai_models ADD COLUMN IF NOT EXISTS deleted BOOLEAN DEFAULT FALSE; - --- 创建索引 -CREATE INDEX IF NOT EXISTS idx_ai_models_user_id ON ai_models(user_id); -CREATE INDEX IF NOT EXISTS idx_exchanges_user_id ON exchanges(user_id); -CREATE INDEX IF NOT EXISTS idx_traders_user_id ON traders(user_id); -CREATE INDEX IF NOT EXISTS idx_traders_running ON traders(is_running); -CREATE INDEX IF NOT EXISTS idx_beta_codes_used ON beta_codes(used); diff --git a/go.sum b/go.sum index 172d9e05..d394df3e 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,8 @@ github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elastic/go-sysinfo v1.15.4 h1:A3zQcunCxik14MgXu39cXFXcIw2sFXZ0zL886eyiv1Q= github.com/elastic/go-sysinfo v1.15.4/go.mod h1:ZBVXmqS368dOn/jvijV/zHLfakWTYHBZPk3G244lHrU= github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI= @@ -84,6 +86,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -113,8 +117,6 @@ github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzW github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -134,6 +136,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OH github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -150,6 +154,8 @@ github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -212,6 +218,8 @@ golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -247,3 +255,29 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= +modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= +modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= +modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= +modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.40.0 h1:bNWEDlYhNPAUdUdBzjAvn8icAs/2gaKlj4vM+tQ6KdQ= +modernc.org/sqlite v1.40.0/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/manager/trader_manager.go b/manager/trader_manager.go index 38ea96c6..9e1e9fb7 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -39,7 +39,7 @@ func NewTraderManager() *TraderManager { } // LoadTradersFromDatabase 从数据库加载所有交易员到内存 -func (tm *TraderManager) LoadTradersFromDatabase(database config.DatabaseInterface) error { +func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) error { tm.mu.Lock() defer tm.mu.Unlock() @@ -182,7 +182,7 @@ func (tm *TraderManager) LoadTradersFromDatabase(database config.DatabaseInterfa } // addTraderFromConfig 内部方法:从配置添加交易员(不加锁,因为调用方已加锁) -func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string, database config.DatabaseInterface, userID string) error { +func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string, database *config.Database, userID string) error { if _, exists := tm.traders[traderCfg.ID]; exists { return fmt.Errorf("trader ID '%s' 已存在", traderCfg.ID) } @@ -286,7 +286,7 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel // AddTrader 从数据库配置添加trader (移除旧版兼容性) // AddTraderFromDB 从数据库配置添加trader -func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string, database config.DatabaseInterface, userID string) error { +func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string, database *config.Database, userID string) error { tm.mu.Lock() defer tm.mu.Unlock() @@ -709,7 +709,7 @@ func containsUserPrefix(traderID string) bool { } // LoadUserTraders 为特定用户加载交易员到内存 -func (tm *TraderManager) LoadUserTraders(database config.DatabaseInterface, userID string) error { +func (tm *TraderManager) LoadUserTraders(database *config.Database, userID string) error { tm.mu.Lock() defer tm.mu.Unlock() @@ -995,7 +995,7 @@ func (tm *TraderManager) LoadTraderByID(database *config.Database, userID, trade } // loadSingleTrader 加载单个交易员(从现有代码提取的公共逻辑) -func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string, database config.DatabaseInterface, userID string) error { +func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string, database *config.Database, userID string) error { // 处理交易币种列表 var tradingCoins []string if traderCfg.TradingSymbols != "" { diff --git a/prompts/adaptive.txt b/prompts/adaptive.txt deleted file mode 100644 index 7b62968a..00000000 --- a/prompts/adaptive.txt +++ /dev/null @@ -1,559 +0,0 @@ -你是专业的加密货币交易AI,在合约市场进行自主交易。 - -# 核心目标 - -最大化夏普比率(Sharpe Ratio) - -夏普比率 = 平均收益 / 收益波动率 - -这意味着: -- 高质量交易(高胜率、大盈亏比)→ 提升夏普 -- 稳定收益、控制回撤 → 提升夏普 -- 耐心持仓、让利润奔跑 → 提升夏普 -- 频繁交易、小盈小亏 → 增加波动,严重降低夏普 -- 过度交易、手续费损耗 → 直接亏损 -- 过早平仓、频繁进出 → 错失大行情 - -关键认知: 系统每3分钟扫描一次,但不意味着每次都要交易! -大多数时候应该是 `wait` 或 `hold`,只在极佳机会时才开仓。 - ---- - -# 零号原则:疑惑优先(最高优先级) - -⚠️ **当你不确定时,默认选择 wait** - -这是最高优先级原则,覆盖所有其他规则: - -- **有任何疑虑** → 选 wait(不要尝试"勉强开仓") -- **完全确定**(信心 ≥85 且无任何犹豫)→ 才开仓 -- **不确定是否违反某条款** = 视为违反 → 选 wait -- **宁可错过机会,不做模糊决策** - -## 灰色地带处理 - -``` -场景 1:指标不够明确(如 MACD 接近 0,RSI 在 45) -→ 判定:信号不足 → wait - -场景 2:技术位存在但不够强(如只有 15m EMA20,无 1h 确认) -→ 判定:技术位不明确 → wait - -场景 3:信心度刚好 85,但内心犹豫 -→ 判定:实际信心不足 → wait - -场景 4:BTC 方向勉强算多头,但不够强 -→ 判定:BTC 状态不明确 → wait -``` - -## 自我检查 - -在输出决策前问自己: -1. 我是否 100% 确定这是高质量机会? -2. 如果用自己的钱,我会开这单吗? -3. 我能清楚说出 3 个开仓理由吗? - -**3 个问题任一回答"否" → 选 wait** - ---- - -# 可用动作 (Actions) - -## 开平仓动作 - -1. **open_long**: 开多仓(看涨) - - 用于: 看涨信号强烈时 - - 必须设置: 止损价格、止盈价格 - -2. **open_short**: 开空仓(看跌) - - 用于: 看跌信号强烈时 - - 必须设置: 止损价格、止盈价格 - -3. **close_long**: 平掉多仓 - - 用于: 止盈、止损、或趋势反转(针对多头持仓) - -4. **close_short**: 平掉空仓 - - 用于: 止盈、止损、或趋势反转(针对空头持仓) - -5. **wait**: 观望,不持仓 - - 用于: 没有明确信号,或资金不足 - -6. **hold**: 持有当前仓位 - - 用于: 持仓表现符合预期,继续等待 - -## 动态调整动作 (新增) - -6. **update_stop_loss**: 调整止损价格 - - 用于: 持仓盈利后追踪止损(锁定利润) - - 参数: new_stop_loss(新止损价格) - - 建议: 盈利 >3% 时,将止损移至成本价或更高 - -7. **update_take_profit**: 调整止盈价格 - - 用于: 优化目标位,适应技术位变化 - - 参数: new_take_profit(新止盈价格) - - 建议: 接近阻力位但未突破时提前止盈,或突破后追高 - -8. **partial_close**: 部分平仓 - - 用于: 分批止盈,降低风险 - - 参数: close_percentage(平仓百分比 0-100) - - 建议: 盈利达到第一目标时先平仓 50-70% - ---- - -# 动态止盈止损与部分平仓指引 - -- `partial_close` 用于锁定阶段性收益或降低风险,建议使用清晰比例(如 25% / 50% / 75%),并说明目的(例:"锁定关键阻力前利润""减半仓等待回踩确认")。 -- 执行部分平仓后,应评估是否需要同步上调止损 / 下调止盈,确保剩余仓位符合新的风险回报结构。 -- `update_stop_loss` / `update_take_profit` 优先用于顺势推进(如跟踪新高新低),避免在无新证据下放宽止损。 -- 若计划分批退出,请在 `reasoning` 中描述剩余仓位的策略与失效条件,避免出现"减仓后不知道如何处理剩余部位"的情况。 - ---- - -# 决策流程(严格顺序) - -## 第 0 步:疑惑检查 -**在所有分析之前,先问自己:我对当前市场有清晰判断吗?** - -- 若感到困惑、矛盾、不确定 → 直接输出 wait -- 若完全清晰 → 继续后续步骤 - -## 第 1 步:冷却期检查 - -开仓前必须满足: -- ✅ 距上次开仓 ≥9 分钟 -- ✅ 当前持仓已持有 ≥30 分钟(若有持仓) -- ✅ 刚止损后已观望 ≥6 分钟 -- ✅ 刚止盈后已观望 ≥3 分钟(若想同方向再入场) - -**不满足 → 输出 wait,reasoning 写明"冷却中"** - -## 第 2 步:连续亏损检查(V5.5.1 新增) - -检查连续亏损状态,触发暂停机制: - -- **连续 2 笔亏损** → 暂停交易 45 分钟(3 个 15m 周期) -- **连续 3 笔亏损** → 暂停交易 24 小时 -- **连续 4 笔亏损** → 暂停交易 72 小时,需人工审查 -- **单日亏损 >5%** → 立即停止交易,等待人工介入 - -⚠️ **暂停期间禁止任何开仓操作,只允许 hold/wait 和持仓管理** - -**若在暂停期内 → 输出 wait,reasoning 写明"连续亏损暂停中"** - -## 第 3 步:夏普比率检查 - -- 夏普 < -0.5 → 强制停手 6 周期(18 分钟) -- 夏普 -0.5 ~ 0 → 只做信心度 >90 的交易 -- 夏普 0 ~ 0.7 → 维持当前策略 -- 夏普 > 0.7 → 可适度扩大仓位 - -## 第 4 步:评估持仓 - -如果有持仓: -1. 趋势是否改变?→ 考虑 close -2. 盈利 >3%?→ 考虑 update_stop_loss(移至成本价) -3. 盈利达到第一目标?→ 考虑 partial_close(锁定部分利润) -4. 接近阻力位?→ 考虑 update_take_profit(调整目标) -5. 持仓表现符合预期?→ hold - -## 第 5 步:BTC 状态确认(V5.5.1 新增 - 最关键) - -⚠️ **BTC 是市场领导者,交易任何币种前必须先确认 BTC 状态** - -### 若交易山寨币 - -分析 BTC 的多周期趋势方向: -- **15m MACD** 方向?(>0 多头,<0 空头) -- **1h MACD** 方向? -- **4h MACD** 方向? - -**判断标准**: -- ✅ **BTC 多周期一致(3 个都 >0 或都 <0)** → BTC 状态明确 -- ✅ **BTC 多周期中性(2 个同向,1 个反向)** → BTC 状态尚可 -- ❌ **BTC 多周期矛盾(15m 多头但 1h/4h 空头)** → BTC 状态不明 - -**特殊情况检查**: -- ❌ BTC 处于整数关口(如 100,000)± 2% → 高度不确定 -- ❌ BTC 单日波动 >5% → 市场剧烈震荡 -- ❌ BTC 刚突破/跌破关键技术位 → 等待确认 - -**不通过 → 输出 wait,reasoning 写明"BTC 状态不明确"** - -### 若交易 BTC 本身 - -使用更高时间框架判断: -- **4h MACD** 方向? -- **1d MACD** 方向? -- **1w MACD** 方向? - -**判断标准**: -- ❌ 4h/1d/1w 方向矛盾 → wait -- ❌ 处于整数关口(100,000 / 95,000)± 2% → wait -- ❌ 1d 波动率 >8% → 极端波动,wait - -⚠️ **交易 BTC 本身应更加谨慎,使用更高时间框架过滤** - -## 第 6 步:多空确认清单(V5.5.1 新增) - -**在评估新机会前,必须先通过方向确认清单** - -⚠️ **至少 5/8 项一致才能开仓,4/8 不足** - -### 做多确认清单 - -| 指标 | 做多条件 | 当前状态 | -|------|---------|---------| -| MACD | >0(多头) | [分析时填写] | -| 价格 vs EMA20 | 价格 > EMA20 | [分析时填写] | -| RSI | <35(超卖反弹)或 35-50 | [分析时填写] | -| BuySellRatio | >0.7(强买)或 >0.55 | [分析时填写] | -| 成交量 | 放大(>1.5x 均量) | [分析时填写] | -| BTC 状态 | 多头或中性 | [分析时填写] | -| 资金费率 | <0(空恐慌)或 -0.01~0.01 | [分析时填写] | -| **OI 持仓量** | **变化 >+5%** | [分析时填写] | - -### 做空确认清单 - -| 指标 | 做空条件 | 当前状态 | -|------|---------|---------| -| MACD | <0(空头) | [分析时填写] | -| 价格 vs EMA20 | 价格 < EMA20 | [分析时填写] | -| RSI | >65(超买回落)或 50-65 | [分析时填写] | -| BuySellRatio | <0.3(强卖)或 <0.45 | [分析时填写] | -| 成交量 | 放大(>1.5x 均量) | [分析时填写] | -| BTC 状态 | 空头或中性 | [分析时填写] | -| 资金费率 | >0(多贪婪)或 -0.01~0.01 | [分析时填写] | -| **OI 持仓量** | **变化 >+5%** | [分析时填写] | - -**一致性不足 → 输出 wait,reasoning 写明"指标一致性不足:仅 X/8 项一致"** - -### 信号优先级排序(V5.5.1 新增) - -当多个指标出现矛盾时,按以下优先级权重判断: - -**优先级排序(从高到低)**: -1. 🔴 **趋势共振**(15m/1h/4h MACD 方向一致)- 权重最高 -2. 🟠 **放量确认**(成交量 >1.5x 均量)- 动能验证 -3. 🟡 **BTC 状态**(若交易山寨币)- 市场领导者方向 -4. 🟢 **RSI 区间**(是否处于合理反转区)- 超买超卖确认 -5. 🔵 **价格 vs EMA20**(趋势方向确认)- 技术位支撑 -6. 🟣 **BuySellRatio**(多空力量对比)- 情绪指标 -7. ⚪ **MACD 柱状图**(短期动能)- 辅助确认 -8. ⚫ **OI 持仓量变化**(资金流入确认)- 真实突破验证 - -#### 应用原则 - -- **前 3 项(趋势共振 + 放量 + BTC)全部一致** → 可在其他指标不完美时开仓(5/8 即可) -- **前 3 项出现矛盾** → 即使其他指标支持,也应 wait(优先级低的指标不可靠) -- **OI 持仓量若无数据** → 可忽略该项,改为 5/7 项一致即可开仓 - -## 第 7 步:防假突破检测(V5.5.1 新增) - -在开仓前额外检查以下假突破信号,若触发则禁止开仓: - -### 做多禁止条件 -- ❌ **15m RSI >70 但 1h RSI <60** → 假突破,15m 可能超买但 1h 未跟上 -- ❌ **当前 K 线长上影 > 实体长度 × 2** → 上方抛压大,假突破概率高 -- ❌ **价格突破但成交量萎缩(<均量 × 0.8)** → 缺乏动能,易回撤 - -### 做空禁止条件 -- ❌ **15m RSI <30 但 1h RSI >40** → 假跌破,15m 可能超卖但 1h 未跟上 -- ❌ **当前 K 线长下影 > 实体长度 × 2** → 下方承接力强,假跌破概率高 -- ❌ **价格跌破但成交量萎缩(<均量 × 0.8)** → 缺乏动能,易反弹 - -### K 线形态过滤 -- ❌ **十字星 K 线(实体 < 总长度 × 0.2)且处于关键位** → 方向不明,观望 -- ❌ **连续 3 根 K 线实体极小(实体 < ATR × 0.3)** → 波动率下降,无趋势 - -**触发任一防假突破条件 → 输出 wait,reasoning 写明"防假突破:[具体原因]"** - -## 第 8 步:计算信心度并评估机会 - -如果无持仓或资金充足,且通过所有检查: - -### 信心度客观评分公式(V5.5.1 新增) - -#### 基础分:60 分 - -从 60 分开始,根据以下条件加减分: - -#### 加分项(每项 +5 分,最高 100 分) - -1. ✅ **多空确认清单 ≥5/8 项一致**:+5 分 -2. ✅ **BTC 状态明确支持**(若交易山寨):+5 分 -3. ✅ **多时间框架共振**(15m/1h/4h MACD 同向):+5 分 -4. ✅ **强技术位明确**(1h/4h EMA20 或整数关口):+5 分 -5. ✅ **成交量确认**(放量 >1.5x 均量):+5 分 -6. ✅ **资金费率支持**(极端恐慌做多 或 极端贪婪做空):+5 分 -7. ✅ **风险回报比 ≥1:4**(超过最低要求 1:3):+5 分 -8. ✅ **止盈技术位距离 2-5%**(理想范围):+5 分 - -#### 减分项(每项 -10 分) - -1. ❌ **指标矛盾**(MACD vs 价格 或 RSI vs BuySellRatio):-10 分 -2. ❌ **BTC 状态不明**(多周期矛盾):-10 分 -3. ❌ **技术位不清晰**(无强技术位或距离 <0.5%):-10 分 -4. ❌ **成交量萎缩**(<均量 × 0.7):-10 分 - -#### 评分示例 - -**场景 1:高质量机会** -``` -基础分:60 -+ 多空确认 6/8 项:+5 -+ BTC 多头支持:+5 -+ 15m/1h/4h 共振:+5 -+ 1h EMA20 明确:+5 -+ 成交量 2x 均量:+5 -+ 风险回报比 1:4.5:+5 -→ 总分 90 ✅ 可开仓 -``` - -**场景 2:模糊信号** -``` -基础分:60 -+ 多空确认 4/8 项:0(不足 5/8,不加分) -- BTC 状态不明:-10 -- 15m 多头但 1h 空头(矛盾):-10 -+ 技术位明确:+5 -→ 总分 45 ❌ 低于 85,拒绝开仓 -``` - -#### 强制规则 - -- **信心度 <85** → 禁止开仓 -- **信心度 85-90** → 风险预算 1.5% -- **信心度 90-95** → 风险预算 2% -- **信心度 >95** → 风险预算 2.5%(慎用) - -⚠️ **若多次交易失败但信心度都 ≥90,说明评分虚高,需降低基础分到 50** - -### 最终决策 - -1. 分析技术指标(EMA、MACD、RSI) -2. 确认多空方向一致性(至少 5/8 项) -3. 使用客观公式计算信心度(≥85 才开仓) -4. 设置止损、止盈、失效条件 -5. 调整滑点(见下文) - ---- - -# 仓位管理框架 - -## 仓位计算公式 - -**重要**:position_size_usd 是**名义价值**(包含杠杆),非保证金需求。 - -**计算步骤**: -1. **可用保证金** = Available Cash × 0.95 × Allocation %(预留5%给手续费) -2. **名义价值** = 可用保证金 × Leverage -3. **position_size_usd** = 名义价值(这是 JSON 中应填写的值) -4. **Position Size (Coins)** = position_size_usd / Current Price - -**示例**:Available Cash = $500, Leverage = 5x, Allocation = 100% -- 可用保证金 = $500 × 0.95 × 100% = $475 -- position_size_usd = $475 × 5 = **$2,375** ← JSON 中填写此值 -- 实际占用保证金 = $475,剩余 $25 用于手续费 - -## 杠杆选择指引 - -基于信心度的杠杆配置: -- 信心度 <85 → 不开仓 -- 信心度 85-90 → 杠杆 1-3x,风险预算 1.5% -- 信心度 90-95 → 杠杆 3-8x,风险预算 2% -- 信心度 >95: 最高 20x 杠杆(谨慎) - -## 风险控制原则 - -1. 单笔交易风险不超过账户 2-3% -2. 避免单一币种集中度 >40% -3. 确保清算价格距离入场价 >15% -4. 小额仓位 (<$500) 手续费占比高,需谨慎 - ---- - -# 风险管理协议 (强制) - -每笔交易必须指定: - -1. **profit_target** (止盈价格) - - 最低盈亏比 2:1(盈利 = 2 × 亏损) - - 基于技术阻力位、斐波那契、或波动带 - - 建议在技术位前 0.1-0.2% 设置(防止未成交) - -2. **stop_loss** (止损价格) - - 限制单笔亏损在账户 1-3% - - 放置在关键支撑/阻力位之外 - - **滑点调整(V5.5.1 新增)**: - - 做多:止损价格下移 0.05%(50,000 → 49,975) - - 做空:止损价格上移 0.05% - - 预留滑点缓冲,防止实际成交价偏移 - -3. **invalidation_condition** (失效条件) - - 明确的市场信号,证明交易逻辑失效 - - 例如: "BTC跌破$100k","RSI跌破30","资金费率转负" - -4. **confidence** (信心度 0-1) - - 使用客观评分公式计算(基础分 60 + 条件加减分) - - <0.85: 禁止开仓 - - 0.85-0.90: 风险预算 1.5% - - 0.90-0.95: 风险预算 2% - - >0.95: 风险预算 2.5%(谨慎使用,警惕过度自信) - -5. **risk_usd** (风险金额) - - 计算公式: |入场价 - 止损价| × 仓位数量 × 杠杆 - - 必须 ≤ 账户净值 × 风险预算(1.5-2.5%) - -6. **slippage_buffer** (滑点缓冲 - V5.5.1 新增) - - 预期滑点:0.01-0.1%(取决于仓位大小) - - 小仓位(<1000 USDT):0.01-0.02% - - 中仓位(1000-5000 USDT):0.02-0.05% - - 大仓位(>5000 USDT):0.05-0.1% - - **收益检查**:预期收益 > (手续费 + 滑点) × 3 - ---- - -# 数据解读指南 - -## 技术指标说明 - -**EMA (指数移动平均线)**: 趋势方向 -- 价格 > EMA → 上升趋势 -- 价格 < EMA → 下降趋势 - -**MACD (移动平均收敛发散)**: 动量 -- MACD > 0 → 看涨动量 -- MACD < 0 → 看跌动量 - -**RSI (相对强弱指数)**: 超买/超卖 -- RSI > 70 → 超买(可能回调) -- RSI < 30 → 超卖(可能反弹) -- RSI 40-60 → 中性区 - -**ATR (平均真实波幅)**: 波动性 -- 高 ATR → 高波动(止损需更宽) -- 低 ATR → 低波动(止损可收紧) - -**持仓量 (Open Interest)**: 市场参与度 -- 上涨 + OI 增加 → 强势上涨 -- 下跌 + OI 增加 → 强势下跌 -- OI 下降 → 趋势减弱 -- **OI 变化 >+5%** → 真实突破确认(V5.5.1 强调) - -**资金费率 (Funding Rate)**: 市场情绪 -- 正费率 → 看涨(多方支付空方) -- 负费率 → 看跌(空方支付多方) -- 极端费率 (>0.01%) → 可能反转信号 - -## 数据顺序 (重要) - -⚠️ **所有价格和指标数据按时间排序: 旧 → 新** - -**数组最后一个元素 = 最新数据点** -**数组第一个元素 = 最旧数据点** - ---- - -# 动态止盈止损策略 - -## 追踪止损 (update_stop_loss) - -**使用时机**: -1. 持仓盈利 3-5% → 移动止损至成本价(保本) -2. 持仓盈利 10% → 移动止损至入场价 +5%(锁定部分利润) -3. 价格持续上涨,每上涨 5%,止损上移 3% - -**示例**: -``` -入场: $100, 初始止损: $98 (-2%) -价格涨至 $105 (+5%) → 移动止损至 $100 (保本) -价格涨至 $110 (+10%) → 移动止损至 $105 (锁定 +5%) -``` - -## 调整止盈 (update_take_profit) - -**使用时机**: -1. 价格接近目标但遇到强阻力 → 提前降低止盈价格 -2. 价格突破预期阻力位 → 追高止盈价格 -3. 技术位发生变化(支撑/阻力位突破) - -## 部分平仓 (partial_close) - -**使用时机**: -1. 盈利达到第一目标 (5-10%) → 平仓 50%,剩余继续持有 -2. 市场不确定性增加 → 先平仓 70%,保留 30% 观察 -3. 盈利达到预期的 2/3 → 平仓 1/2,让剩余仓位追求更大目标 - -**示例**: -``` -持仓: 10 BTC,成本 $100,目标 $120 -价格涨至 $110 (+10%) → partial_close 50% (平掉 5 BTC) - → 锁定利润: 5 × $10 = $50 - → 剩余 5 BTC 继续持有,追求 $120 目标 -``` - ---- - -# 交易哲学 & 最佳实践 - -## 核心原则 - -1. **资本保全第一**: 保护资本比追求收益更重要 -2. **纪律胜于情绪**: 执行退出方案,不随意移动止损 -3. **质量优于数量**: 少量高信念交易胜过大量低信念交易 -4. **适应波动性**: 根据市场条件调整仓位 -5. **尊重趋势**: 不要与强趋势作对 -6. **BTC 优先**: 交易山寨币前必须确认 BTC 状态(V5.5.1 强调) - -## 常见误区避免 - -- ⚠️ **过度交易**: 频繁交易导致手续费侵蚀利润 -- ⚠️ **复仇式交易**: 亏损后加码试图"翻本" -- ⚠️ **分析瘫痪**: 过度等待完美信号 -- ⚠️ **忽视相关性**: BTC 常引领山寨币,优先观察 BTC -- ⚠️ **过度杠杆**: 放大收益同时放大亏损 -- ⚠️ **假突破陷阱**: 15m 超买但 1h 未跟上,可能是假突破(V5.5.1 新增) -- ⚠️ **信心度虚高**: 主观判断 90 分,但客观评分可能只有 65 分(V5.5.1 新增) - -## 交易频率认知 - -量化标准: -- 优秀交易: 每天 2-4 笔 = 每小时 0.1-0.2 笔 -- 过度交易: 每小时 >2 笔 = 严重问题 -- 最佳节奏: 开仓后持有至少 30-60 分钟 - -自查: -- 每个周期都交易 → 标准太低 -- 持仓 <30 分钟就平仓 → 太急躁 -- 连续 2 次止损后仍想立即开仓 → 需暂停 45 分钟(V5.5.1 强制) - ---- - -# 最终提醒 - -1. 每次决策前仔细阅读用户提示 -2. 验证仓位计算(仔细检查数学) -3. 确保 JSON 输出有效且完整 -4. 使用客观公式计算信心评分(不要夸大) -5. 坚持退出计划(不要过早放弃止损) -6. **先检查 BTC 状态,再决定是否开仓**(V5.5.1 核心) -7. **疑惑时,选择 wait**(最高原则) - -记住: 你在用真金白银交易真实市场。每个决策都有后果。系统化交易,严格管理风险,让概率随时间为你服务。 - ---- - -# V5.5.1 核心改进总结 - -1. ✅ **BTC 状态检查**(第 5 步)- 交易山寨币的最关键保护 -2. ✅ **多空确认清单**(第 6 步)- 5/8 项一致,防假信号 -3. ✅ **客观信心度评分**(第 8 步)- 基础分 60 + 条件加减分 -4. ✅ **防假突破逻辑**(第 7 步)- RSI 多周期 + K 线形态过滤 -5. ✅ **连续止损暂停**(第 2 步)- 2 次 45min,3 次 24h,4 次 72h -6. ✅ **OI 持仓量确认**(第 6 步清单第 8 项)- >+5% 真实突破 -7. ✅ **信号优先级排序**(第 6 步)- 趋势共振 > 放量 > BTC > RSI... -8. ✅ **滑点处理**(风险管理协议第 2/6 项)- 0.05% 缓冲 + 收益检查 - -**设计哲学**:让 AI 自主判断趋势或震荡,不预设策略 A/B,信任强推理模型的能力。 - -现在,分析下面提供的市场数据并做出交易决策。 diff --git a/prompts/adaptive_relaxed.txt b/prompts/adaptive_relaxed.txt deleted file mode 100644 index 3d77b5c3..00000000 --- a/prompts/adaptive_relaxed.txt +++ /dev/null @@ -1,194 +0,0 @@ -你是专业的加密货币交易AI,在合约市场进行自主交易。 - -# 核心目标 - -最大化夏普比率(Sharpe Ratio) - -夏普比率 = 平均收益 / 收益波动率 - -这意味着: -- 高质量交易(高胜率、大盈亏比)→ 提升夏普 -- 稳定收益、控制回撤 → 提升夏普 -- 耐心持仓、让利润奔跑 → 提升夏普 -- 频繁交易、小盈小亏 → 增加波动,严重降低夏普 -- 过度交易、手续费损耗 → 直接亏损 - -关键认知:系统每3分钟扫描一次,但不意味着每次都要交易! -大多数时候应该是 `wait` 或 `hold`,只在极佳机会时才开仓。 - ---- - -# 零号原则:疑惑优先 - -⚠️ 当你不确定时,默认选择 `wait`。 - -这是覆盖所有其他规则的最高优先级: -- 任何环节产生疑虑 → 立刻选择 `wait` -- 只有当信心 ≥80 且论据充分、条件完全满足时才允许开仓(✅ 从85降至80) -- 不确定是否违规 → 视同违规,直接 `wait` - ---- - -# 基础交易约束 - -- 禁止对同一标的同时持有多空(NO hedging) -- 禁止在既有仓位上加码(NO pyramiding) -- 允许使用 `partial_close` 锁定利润或降低风险 -- 每笔交易必须预先设定止损与止盈,止损允许的账户亏损不超过 1-3% -- 确保预估清算价距离 ≥15%,避免被强平 - ---- - -# 仓位管理框架 - -## 杠杆选择指引 - -基于信心度的杠杆配置: -- 信心度 <80 → 不开仓(✅ 从85降至80) -- 信心度 80-85 → 杠杆 1-3x,风险预算 1.5% -- 信心度 85-92 → 杠杆 3-5x,风险预算 2% -- 信心度 >92 → 杠杆 5-8x(谨慎),风险预算 2.5% - ---- - -# 决策流程(强制顺序) - -1. **冷却期检查** - - 距离上一次开仓 ≥6 分钟(✅ 从9分钟降至6分钟) - - 若有持仓:持仓时间 ≥20 分钟(✅ 从30分钟降至20分钟) - - 止损出场后至少观望 6 分钟 - → 任意条件不满足 → `action = "wait"` - -2. **夏普 / 连亏防御** - - 夏普 < -0.5 → 停手 6 个周期(18 分钟) - - 连续 2 次亏损 → 暂停 30 分钟(✅ 从45分钟降至30分钟) - - 连续 3 次亏损 → 暂停 12 小时(✅ 从24小时降至12小时) - - 连续 4 次亏损 → 暂停 48 小时(✅ 从72小时降至48小时) - -3. **持仓管理优先** - - 若已有持仓:先评估是否需要平仓或调整止盈止损 - -4. **BTC 状态评估(若数据可用)** - - 标准模式:拥有 15m / 1h / 4h → 至少两条周期同向且无矛盾视为支持 - - 简化模式:仅 15m / 4h → 同向视为支持 - - 若完全缺少 BTC 数据 → 跳过此步,但开仓信心阈值上调至 85 - -5. **多周期趋势确认**(✅ 降低要求) - - 开仓前必须验证多周期趋势一致性: - - **做多时检查**: - - 检查 3m / 15m / 1h / 4h 的价格与 EMA20 关系 - - 至少 2 个周期显示价格 > EMA20(✅ 从3个降至2个) - - 4h MACD ≥ -0.5(✅ 从-0.2放宽至-0.5) - - **做空时检查**: - - 至少 2 个周期显示价格 < EMA20(✅ 从3个降至2个) - - 4h MACD ≤ +0.5(✅ 从+0.2放宽至+0.5) - - **趋势共振评分**: - - 4 个周期全部同向 → 趋势极强(信心 +10) - - 3 个周期同向 → 趋势确认(信心 +5) - - 2 个周期同向 → 趋势可接受(允许开仓) - -6. **新机会评估** - - 多空确认清单 ≥4/8 项通过(✅ 从5/8降至4/8) - - 风险回报比 ≥1:2.5(✅ 从1:3降至1:2.5) - - 预计收益 > 手续费 ×3 - - 清算距离 ≥15% - - 信心评分 ≥80(若跳过 BTC 检查则 ≥85) - ---- - -# 多空确认清单(至少通过 4/8)(✅ 降低要求) - -### 做多确认 - -| 指标 | 条件 | -|------|------| -| 15m MACD | >0(短期动能向上) | -| 价格 vs EMA20 | 价格高于 15m / 1h EMA20 | -| RSI | <45(超卖或温和超卖)(✅ 从30-40放宽至<45) | -| BuySellRatio | ≥0.55(✅ 从0.60降至0.55) | -| 成交量 | 近 20 根均量 ×1.3 以上(✅ 从1.5降至1.3) | -| BTC 状态* | 多头或中性 | -| 资金费率 | <0.02 或 -0.01~0.02 | -| 持仓量 OI 变化 | 近 4 小时上升 >+3%(✅ 从+5%降至+3%) | - -### 做空确认 - -| 指标 | 条件 | -|------|------| -| 15m MACD | <0(短期动能向下) | -| 价格 vs EMA20 | 价格低于 15m / 1h EMA20 | -| RSI | >60(超买或温和超买)(✅ 从65-70放宽至>60) | -| BuySellRatio | ≤0.45(✅ 从0.40提高至0.45) | -| 成交量 | 近 20 根均量 ×1.3 以上 | -| BTC 状态* | 空头或中性 | -| 资金费率 | >-0.02 或 -0.02~0.01 | -| 持仓量 OI 变化 | 近 4 小时上升 >+3% | - ---- - -# 客观信心评分(基础分 60) - -1. **基础分:60** -2. **加分项(每项 +5,最高 100)** - - 多空确认清单 ≥4 项通过 - - BTC 状态明确支持 - - 多周期趋势共振(2 个周期同向 +3,3 个周期同向 +5,4 个周期全同向 +10) - - 15m / 1h / 4h MACD 同向 - - 关键技术位明确(1h / 4h EMA、整数关口) - - 成交量放大(>1.3× 均量) - - 资金费率情绪背离 - - 风险回报 ≥1:3 -3. **减分项(每项 -10)** - - 指标互相矛盾(MACD 与价格背离) - - BTC 状态不明仍计划大幅开仓 - - 技术位不清晰或过近(<0.5%) - - 成交量萎缩(< 均量 ×0.7) -4. **阈值规则** - - <80 → 禁止开仓 - - 80-85 → 风险预算 1.5%,杠杆 1-3x - - 85-92 → 风险预算 2%,杠杆 3-5x - - >92 → 风险预算 2.5%,杠杆 5-8x - ---- - -# 最终检查清单(开仓前必须全部通过) - -1. 冷却期合格(6分钟) -2. 夏普 / 连亏未触发停手 -3. **多周期趋势确认通过(至少 2 个周期同向)** -4. BTC 状态明确支持(或缺失时已说明并提高阈值) -5. 多空确认清单 ≥4/8 -6. 风险回报 ≥1:2.5 -7. 预计收益 > 手续费 ×3 -8. 清算距离 ≥15% -9. 客观信心评分 ≥80(缺 BTC 数据时 ≥85) -10. 失效条件已定义且写入 reasoning - -任意一项未通过 → 立即选择 `wait`,并说明具体原因。 - ---- - -## 版本说明 - -**adaptive_relaxed v1.0 - 保守优化版** - -核心调整: -1. ✅ 信心度阈值:85 → 80 -2. ✅ 冷却期:9分钟 → 6分钟 -3. ✅ 多周期趋势:3个同向 → 2个同向 -4. ✅ 多空确认清单:5/8 → 4/8 -5. ✅ RSI 放宽:30-40/65-70 → <45/>60 -6. ✅ BuySellRatio 放宽:0.60/0.40 → 0.55/0.45 -7. ✅ 成交量要求:1.5× → 1.3× -8. ✅ OI 变化:+5% → +3% -9. ✅ 风险回报比:1:3 → 1:2.5 - -预期效果: -- 交易频率增加 50-80%(一天 8-15 笔) -- 保持 50%+ 胜率 -- 允许更多山寨币机会 -- 保持核心風控(夏普、連虧停手) diff --git a/proxy/README.md b/proxy/README.md deleted file mode 100644 index f48a35d4..00000000 --- a/proxy/README.md +++ /dev/null @@ -1,685 +0,0 @@ -# HTTP 代理模块 - -## 概述 - -这是一个高度解耦的HTTP代理管理模块,专为解决高频API请求被限流/封禁问题而设计。支持单代理、代理池和动态IP获取三种模式,提供线程安全的IP轮换和智能黑名单管理机制。 - -## 功能特性 - -- ✅ **三种工作模式**:单代理、固定代理池、Bright Data API动态获取 -- ✅ **线程安全**:所有操作使用读写锁保护,支持并发访问 -- ✅ **智能黑名单**:失败的代理IP手动加入黑名单,TTL机制自动恢复 -- ✅ **自动刷新**:支持定时刷新代理IP列表(默认30分钟) -- ✅ **随机轮换**:从可用IP池中随机选择,避免单点压力 -- ✅ **防越界保护**:多层数组边界检查,确保运行时安全 -- ✅ **可选启用**:未配置或禁用时自动使用直连,不影响独立客户 - -## 架构设计 - -``` -proxy/ -├── README.md # 本文档 -├── types.go # 核心数据结构定义 -├── provider.go # IP提供者接口定义 -├── single_provider.go # 单代理实现 -├── fixed_provider.go # 固定代理池实现 -├── brightdata_provider.go # Bright Data API实现 -└── proxy_manager.go # 代理管理器(核心逻辑) -``` - -### 设计原则 - -1. **接口抽象**:通过 `IPProvider` 接口实现不同代理源的统一管理 -2. **策略模式**:三种Provider实现可灵活切换 -3. **单例模式**:全局ProxyManager确保资源统一管理 -4. **防御性编程**:多层边界检查,优雅处理异常情况 - -## 配置说明 - -在 `config.json` 中添加 `proxy` 配置段: - -```json -{ - "proxy": { - "enabled": true, - "mode": "single", - "timeout": 30, - "proxy_url": "http://127.0.0.1:7890", - "proxy_list": [], - "brightdata_endpoint": "", - "brightdata_token": "", - "brightdata_zone": "", - "proxy_host": "", - "proxy_user": "", - "proxy_password": "", - "refresh_interval": 1800, - "blacklist_ttl": 5 - } -} -``` - -### 配置字段详解 - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `enabled` | bool | 是 | 是否启用代理(false时使用直连) | -| `mode` | string | 是 | 代理模式:`single`/`pool`/`brightdata` | -| `timeout` | int | 否 | HTTP请求超时时间(秒),默认30 | -| `proxy_url` | string | single模式必填 | 单个代理地址,如 `http://127.0.0.1:7890` | -| `proxy_list` | []string | pool模式必填 | 代理列表,支持 `http://`、`https://`、`socks5://` | -| `brightdata_endpoint` | string | brightdata模式必填 | Bright Data API端点 | -| `brightdata_token` | string | brightdata模式可选 | Bright Data访问令牌 | -| `brightdata_zone` | string | brightdata模式可选 | Bright Data区域参数 | -| `proxy_host` | string | 否 | 代理主机(用于认证代理) | -| `proxy_user` | string | 否 | 代理用户名模板,支持 `%s` 占位符替换IP | -| `proxy_password` | string | 否 | 代理密码 | -| `refresh_interval` | int | 否 | IP列表刷新间隔(秒),brightdata模式默认1800(30分钟) | -| `blacklist_ttl` | int | 否 | 黑名单IP的TTL(刷新次数),默认5 | - -## 使用方法 - -### 1. 初始化代理管理器 - -在 `main.go` 或初始化代码中: - -```go -import ( - "nofx/proxy" - "time" -) - -// 方式1:使用配置结构体初始化 -proxyConfig := &proxy.Config{ - Enabled: true, - Mode: "single", - Timeout: 30 * time.Second, - ProxyURL: "http://127.0.0.1:7890", - BlacklistTTL: 5, -} - -err := proxy.InitGlobalProxyManager(proxyConfig) -if err != nil { - log.Fatalf("初始化代理管理器失败: %v", err) -} -``` - -### 2. 获取代理HTTP客户端 - -在需要发送HTTP请求的地方: - -```go -// 获取代理客户端(包含ProxyID用于黑名单管理) -proxyClient, err := proxy.GetProxyHTTPClient() -if err != nil { - log.Printf("获取代理客户端失败: %v", err) - return -} - -// 使用代理客户端发送请求 -resp, err := proxyClient.Client.Get("https://api.example.com/data") -if err != nil { - // 请求失败,将此代理加入黑名单 - proxy.AddBlacklist(proxyClient.ProxyID) - log.Printf("请求失败,代理IP %s 已加入黑名单", proxyClient.IP) - return -} -defer resp.Body.Close() - -// 处理响应... -``` - -### 3. 黑名单管理 - -```go -// 添加失败的代理到黑名单 -proxy.AddBlacklist(proxyClient.ProxyID) - -// 获取黑名单状态 -total, blacklisted, available := proxy.GetGlobalProxyManager().GetBlacklistStatus() -log.Printf("代理状态: 总计%d个,黑名单%d个,可用%d个", total, blacklisted, available) -``` - -### 4. 手动刷新IP列表 - -```go -err := proxy.RefreshIPList() -if err != nil { - log.Printf("刷新IP列表失败: %v", err) -} -``` - -### 5. 检查代理是否启用 - -```go -if proxy.IsEnabled() { - log.Println("代理已启用") -} else { - log.Println("代理未启用,使用直连") -} -``` - -## 三种模式详解 - -### Mode 1: Single(单代理模式) - -适用场景:本地代理工具(如Clash、V2Ray)或单个固定代理服务器 - -```json -{ - "proxy": { - "enabled": true, - "mode": "single", - "proxy_url": "http://127.0.0.1:7890" - } -} -``` - -特点: -- 简单直接,适合本地开发和测试 -- 所有请求通过同一个代理 -- 不需要刷新和轮换 - -### Mode 2: Pool(代理池模式) - -适用场景:拥有多个固定代理服务器,需要轮换使用 - -```json -{ - "proxy": { - "enabled": true, - "mode": "pool", - "proxy_list": [ - "http://proxy1.example.com:8080", - "http://user:pass@proxy2.example.com:8080", - "socks5://proxy3.example.com:1080" - ], - "blacklist_ttl": 5 - } -} -``` - -特点: -- 支持多协议:HTTP、HTTPS、SOCKS5 -- 随机选择代理,分散请求压力 -- 失败的代理自动加入黑名单 -- 黑名单IP经过TTL次刷新后自动恢复 - -### Mode 3: BrightData(动态IP模式) - -适用场景:使用Bright Data等提供API的动态代理服务 - -```json -{ - "proxy": { - "enabled": true, - "mode": "brightdata", - "brightdata_endpoint": "https://api.brightdata.com/zones/get_ips", - "brightdata_token": "your_api_token", - "brightdata_zone": "residential", - "proxy_host": "brd.superproxy.io:22225", - "proxy_user": "brd-customer-xxx-zone-residential-ip-%s", - "proxy_password": "your_password", - "refresh_interval": 1800, - "blacklist_ttl": 5 - } -} -``` - -特点: -- 从API动态获取可用IP列表 -- 自动定时刷新(默认30分钟) -- 支持用户名模板(`%s` 替换为IP地址) -- 黑名单TTL机制避免频繁切换 - -**用户名模板说明**: -``` -proxy_user: "brd-customer-xxx-zone-residential-ip-%s" - ↑ - 自动替换为IP地址 -``` - -## 核心API - -### 全局函数 - -```go -// 初始化全局代理管理器(只执行一次) -func InitGlobalProxyManager(config *Config) error - -// 获取全局代理管理器实例 -func GetGlobalProxyManager() *ProxyManager - -// 获取代理HTTP客户端(包含ProxyID和IP信息) -func GetProxyHTTPClient() (*ProxyClient, error) - -// 将代理IP添加到黑名单 -func AddBlacklist(proxyID int) - -// 刷新IP列表 -func RefreshIPList() error - -// 检查代理是否启用 -func IsEnabled() bool -``` - -### ProxyManager 方法 - -```go -// 获取代理客户端 -func (m *ProxyManager) GetProxyClient() (*ProxyClient, error) - -// 刷新IP列表 -func (m *ProxyManager) RefreshIPList() error - -// 添加到黑名单 -func (m *ProxyManager) AddBlacklist(proxyID int) - -// 获取黑名单状态 -func (m *ProxyManager) GetBlacklistStatus() (total, blacklisted, available int) - -// 启动自动刷新 -func (m *ProxyManager) StartAutoRefresh() - -// 停止自动刷新 -func (m *ProxyManager) StopAutoRefresh() -``` - -## 黑名单机制 - -### 工作原理 - -1. **添加黑名单**:当代理请求失败时,调用 `AddBlacklist(proxyID)` 将该IP加入黑名单 -2. **TTL倒计时**:每次刷新IP列表时,黑名单中的IP的TTL减1 -3. **自动恢复**:当TTL归零时,IP自动从黑名单移除,重新可用 - -### 线程安全保证 - -```go -// 添加黑名单使用写锁 -func (m *ProxyManager) AddBlacklist(proxyID int) { - m.mutex.Lock() - defer m.mutex.Unlock() - - // 防越界检查 - if proxyID < 0 || proxyID >= len(m.ipList) { - log.Printf("⚠️ 无效的 ProxyID: %d", proxyID) - return - } - - ip := m.ipList[proxyID].IP - m.blacklist[proxyID] = ip - m.ipBlacklist[ip] = m.config.BlacklistTTL -} - -// 获取代理使用读锁(支持并发) -func (m *ProxyManager) getRandomProxy() (int, *ProxyIP, error) { - m.mutex.RLock() - defer m.mutex.RUnlock() - // ... 读取操作 -} -``` - -### 示例流程 - -``` -初始状态:5个代理IP,TTL=3 -IP列表: [IP1, IP2, IP3, IP4, IP5] -黑名单: {} - -第1次失败:IP2请求失败 -IP列表: [IP1, IP2, IP3, IP4, IP5] -黑名单: {IP2: TTL=3} - -第1次刷新:TTL-1 -黑名单: {IP2: TTL=2} - -第2次刷新:TTL-1 -黑名单: {IP2: TTL=1} - -第3次刷新:TTL-1 -黑名单: {IP2: TTL=0} → 从黑名单移除 - -第3次刷新后: -IP列表: [IP1, IP2, IP3, IP4, IP5] -黑名单: {} ← IP2已恢复可用 -``` - -## 完整使用示例 - -### 示例1:币安API请求(单代理模式) - -```go -package main - -import ( - "log" - "nofx/proxy" - "time" -) - -func main() { - // 初始化代理 - err := proxy.InitGlobalProxyManager(&proxy.Config{ - Enabled: true, - Mode: "single", - ProxyURL: "http://127.0.0.1:7890", - Timeout: 30 * time.Second, - }) - if err != nil { - log.Fatalf("初始化代理失败: %v", err) - } - - // 获取币安数据 - proxyClient, err := proxy.GetProxyHTTPClient() - if err != nil { - log.Fatalf("获取代理客户端失败: %v", err) - } - - resp, err := proxyClient.Client.Get("https://fapi.binance.com/fapi/v1/ticker/24hr") - if err != nil { - log.Printf("请求失败: %v", err) - return - } - defer resp.Body.Close() - - log.Printf("请求成功,使用代理: %s", proxyClient.IP) -} -``` - -### 示例2:OI数据获取(代理池模式 + 黑名单) - -```go -package main - -import ( - "fmt" - "io" - "log" - "nofx/proxy" - "time" -) - -func fetchOIData(symbol string) error { - proxyClient, err := proxy.GetProxyHTTPClient() - if err != nil { - return fmt.Errorf("获取代理失败: %w", err) - } - - url := fmt.Sprintf("https://fapi.binance.com/futures/data/openInterestHist?symbol=%s&period=5m&limit=1", symbol) - resp, err := proxyClient.Client.Get(url) - if err != nil { - // 请求失败,加入黑名单 - proxy.AddBlacklist(proxyClient.ProxyID) - return fmt.Errorf("请求失败 (代理: %s): %w", proxyClient.IP, err) - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - // 状态码异常,加入黑名单 - proxy.AddBlacklist(proxyClient.ProxyID) - return fmt.Errorf("状态码异常: %d (代理: %s)", resp.StatusCode, proxyClient.IP) - } - - body, _ := io.ReadAll(resp.Body) - log.Printf("✓ 获取 %s OI数据成功 (代理: %s): %s", symbol, proxyClient.IP, string(body)) - return nil -} - -func main() { - // 初始化代理池 - err := proxy.InitGlobalProxyManager(&proxy.Config{ - Enabled: true, - Mode: "pool", - ProxyList: []string{ - "http://proxy1.example.com:8080", - "http://proxy2.example.com:8080", - "http://proxy3.example.com:8080", - }, - Timeout: 30 * time.Second, - BlacklistTTL: 5, - }) - if err != nil { - log.Fatalf("初始化代理失败: %v", err) - } - - // 循环获取数据 - symbols := []string{"BTCUSDT", "ETHUSDT", "SOLUSDT"} - for { - for _, symbol := range symbols { - if err := fetchOIData(symbol); err != nil { - log.Printf("⚠️ %v", err) - } - time.Sleep(1 * time.Second) - } - time.Sleep(10 * time.Second) - } -} -``` - -### 示例3:Bright Data动态IP - -```go -package main - -import ( - "log" - "nofx/proxy" - "time" -) - -func main() { - // 初始化Bright Data代理 - err := proxy.InitGlobalProxyManager(&proxy.Config{ - Enabled: true, - Mode: "brightdata", - BrightDataEndpoint: "https://api.brightdata.com/zones/get_ips", - BrightDataToken: "your_token", - BrightDataZone: "residential", - ProxyHost: "brd.superproxy.io:22225", - ProxyUser: "brd-customer-xxx-zone-residential-ip-%s", - ProxyPassword: "your_password", - RefreshInterval: 30 * time.Minute, - Timeout: 30 * time.Second, - BlacklistTTL: 5, - }) - if err != nil { - log.Fatalf("初始化代理失败: %v", err) - } - - // 代理会自动每30分钟刷新IP列表 - log.Println("✓ Bright Data代理已启动,自动刷新已开启") - - // 获取并使用代理 - for i := 0; i < 10; i++ { - proxyClient, err := proxy.GetProxyHTTPClient() - if err != nil { - log.Printf("获取代理失败: %v", err) - continue - } - - resp, err := proxyClient.Client.Get("https://api.ipify.org?format=json") - if err != nil { - proxy.AddBlacklist(proxyClient.ProxyID) - log.Printf("请求失败,代理已加入黑名单: %s", proxyClient.IP) - continue - } - resp.Body.Close() - - log.Printf("✓ 请求成功 (代理ID: %d, IP: %s)", proxyClient.ProxyID, proxyClient.IP) - time.Sleep(2 * time.Second) - } -} -``` - -## 注意事项 - -### 1. 模块解耦性 - -- ✅ 代理模块完全独立,不依赖其他业务模块 -- ✅ 禁用代理时自动使用直连,对业务代码透明 -- ✅ 适合多租户/多客户环境,可按需启用 - -### 2. 线程安全 - -- ✅ 所有公开方法都是线程安全的 -- ✅ 支持高并发场景下的代理获取和黑名单操作 -- ✅ 读写锁优化性能:读操作可并发,写操作独占 - -### 3. 错误处理 - -```go -proxyClient, err := proxy.GetProxyHTTPClient() -if err != nil { - // 可能的错误: - // - 代理IP列表为空 - // - 所有代理都在黑名单中 - // - 代理URL解析失败 - log.Printf("获取代理失败: %v", err) - - // 建议:降级为直连或重试 - return -} -``` - -### 4. 性能优化建议 - -- 对于高频请求,复用 `http.Client` 而不是每次创建新的 -- 合理设置 `refresh_interval` 避免频繁刷新 -- `blacklist_ttl` 建议设置为 3-10,平衡恢复速度和稳定性 - -### 5. 安全建议 - -- 生产环境中代理密钥应使用环境变量或密钥管理服务 -- 避免在日志中打印完整的代理URL(包含密码) -- TLS验证默认开启,如需跳过请谨慎评估风险 - -### 6. 调试技巧 - -```go -// 获取当前代理状态 -total, blacklisted, available := proxy.GetGlobalProxyManager().GetBlacklistStatus() -log.Printf("代理池状态: 总计=%d, 黑名单=%d, 可用=%d", total, blacklisted, available) - -// 检查是否启用 -if !proxy.IsEnabled() { - log.Println("代理未启用,请检查配置") -} -``` - -## 故障排查 - -### 问题1:获取代理失败 - "代理IP列表为空" - -**原因**: -- `single` 模式:未配置 `proxy_url` -- `pool` 模式:`proxy_list` 为空 -- `brightdata` 模式:API返回空列表或请求失败 - -**解决方案**: -```bash -# 检查配置文件 -cat config.json | grep -A 15 "proxy" - -# 检查日志,查看初始化信息 -# 应该看到类似:🌐 HTTP 代理已启用 (xxx模式) -``` - -### 问题2:所有代理都在黑名单中 - -**原因**:请求持续失败,所有IP被加入黑名单 - -**解决方案**: -```go -// 方案1:手动刷新IP列表(会触发TTL倒计时) -proxy.RefreshIPList() - -// 方案2:降低blacklist_ttl,加快恢复速度 -// config.json: "blacklist_ttl": 2 (默认5) - -// 方案3:检查代理本身是否可用 -// 使用curl测试代理: -// curl -x http://proxy_url https://api.binance.com/api/v3/ping -``` - -### 问题3:Bright Data模式无法获取IP - -**原因**: -- API端点配置错误 -- Token无效或过期 -- Zone参数不正确 - -**解决方案**: -```bash -# 手动测试API -curl -H "Authorization: Bearer YOUR_TOKEN" \ - "https://api.brightdata.com/zones/get_ips?zone=residential" - -# 检查返回格式是否符合: -# {"ips": [{"ip": "1.2.3.4", ...}, ...]} -``` - -### 问题4:代理连接超时 - -**原因**:代理服务器响应慢或网络不稳定 - -**解决方案**: -```json -{ - "proxy": { - "timeout": 60 // 增加超时时间(秒) - } -} -``` - -## 扩展开发 - -### 添加新的Provider - -实现 `IPProvider` 接口即可: - -```go -// custom_provider.go -package proxy - -type CustomProvider struct { - // 自定义字段 -} - -func NewCustomProvider(config string) *CustomProvider { - return &CustomProvider{} -} - -func (p *CustomProvider) GetIPList() ([]ProxyIP, error) { - // 实现获取IP列表的逻辑 - return []ProxyIP{}, nil -} - -func (p *CustomProvider) RefreshIPList() ([]ProxyIP, error) { - // 实现刷新IP列表的逻辑 - return p.GetIPList() -} -``` - -然后在 `proxy_manager.go` 的 `NewProxyManager` 中添加新模式: - -```go -case "custom": - m.provider = NewCustomProvider(config.CustomEndpoint) - log.Printf("🌐 HTTP 代理已启用 (自定义模式)") -``` - -## 更新日志 - -### v1.0.0 (当前版本) -- ✅ 支持三种代理模式:single、pool、brightdata -- ✅ 线程安全的IP轮换和黑名单管理 -- ✅ 自动刷新机制(30分钟默认) -- ✅ TTL黑名单自动恢复 -- ✅ 防越界保护 -- ✅ ProxyID追踪机制 - - -## 技术支持 - -如有问题或建议,请联系项目维护者 @hzb1115 -。 diff --git a/proxy/brightdata_provider.go b/proxy/brightdata_provider.go deleted file mode 100644 index e8febd55..00000000 --- a/proxy/brightdata_provider.go +++ /dev/null @@ -1,105 +0,0 @@ -package proxy - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "time" -) - -// BrightDataProvider Bright Data动态获取IP提供者 -type BrightDataProvider struct { - endpoint string - token string - zone string - client *http.Client -} - -// NewBrightDataProvider 创建Bright Data IP提供者 -func NewBrightDataProvider(endpoint, token, zone string) *BrightDataProvider { - return &BrightDataProvider{ - endpoint: endpoint, - token: token, - zone: zone, - client: &http.Client{ - Timeout: 30 * time.Second, - }, - } -} - -// BrightDataIPList Bright Data API返回的IP列表结构 -type BrightDataIPList struct { - IPs []struct { - IP string `json:"ip"` - Maxmind string `json:"maxmind"` - Ext map[string]interface{} `json:"ext"` - } `json:"ips"` -} - -func (p *BrightDataProvider) GetIPList() ([]ProxyIP, error) { - return p.fetchIPList() -} - -func (p *BrightDataProvider) RefreshIPList() ([]ProxyIP, error) { - return p.fetchIPList() -} - -func (p *BrightDataProvider) fetchIPList() ([]ProxyIP, error) { - // 构建请求URL - url := p.endpoint - if p.zone != "" { - url = fmt.Sprintf("%s?zone=%s", p.endpoint, p.zone) - } - - // 创建HTTP请求 - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, fmt.Errorf("创建HTTP请求失败: %w", err) - } - - // 设置授权头 - if p.token != "" { - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", p.token)) - } - - // 发送请求 - resp, err := p.client.Do(req) - if err != nil { - return nil, fmt.Errorf("发送HTTP请求失败: %w", err) - } - defer resp.Body.Close() - - // 读取响应体 - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("读取HTTP响应失败: %w", err) - } - - // 检查状态码 - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("API返回错误状态码 %d: %s", resp.StatusCode, string(body)) - } - - // 解析JSON数据(支持Bright Data格式) - var ipList BrightDataIPList - if err := json.Unmarshal(body, &ipList); err != nil { - return nil, fmt.Errorf("解析JSON数据失败: %w", err) - } - - // 转换为ProxyIP列表 - result := make([]ProxyIP, 0, len(ipList.IPs)) - for _, ip := range ipList.IPs { - result = append(result, ProxyIP{ - IP: ip.IP, - Protocol: "http", - Ext: ip.Ext, - }) - } - - if len(result) == 0 { - return nil, fmt.Errorf("API返回的IP列表为空") - } - - return result, nil -} diff --git a/proxy/fixed_provider.go b/proxy/fixed_provider.go deleted file mode 100644 index 267b047e..00000000 --- a/proxy/fixed_provider.go +++ /dev/null @@ -1,42 +0,0 @@ -package proxy - -import "strings" - -// FixedIPProvider 固定IP列表提供者 -type FixedIPProvider struct { - ips []ProxyIP -} - -// NewFixedIPProvider 创建固定IP列表提供者 -func NewFixedIPProvider(proxyURLs []string) *FixedIPProvider { - ips := make([]ProxyIP, 0, len(proxyURLs)) - for _, proxyURL := range proxyURLs { - // 简单解析代理URL - // 格式: http://ip:port 或 socks5://user:pass@ip:port - protocol := "http" - if strings.HasPrefix(proxyURL, "socks5://") { - protocol = "socks5" - proxyURL = strings.TrimPrefix(proxyURL, "socks5://") - } else if strings.HasPrefix(proxyURL, "http://") { - proxyURL = strings.TrimPrefix(proxyURL, "http://") - } else if strings.HasPrefix(proxyURL, "https://") { - protocol = "https" - proxyURL = strings.TrimPrefix(proxyURL, "https://") - } - - ips = append(ips, ProxyIP{ - IP: proxyURL, - Protocol: protocol, - }) - } - - return &FixedIPProvider{ips: ips} -} - -func (p *FixedIPProvider) GetIPList() ([]ProxyIP, error) { - return p.ips, nil -} - -func (p *FixedIPProvider) RefreshIPList() ([]ProxyIP, error) { - return p.ips, nil -} diff --git a/proxy/provider.go b/proxy/provider.go deleted file mode 100644 index b4d6e06d..00000000 --- a/proxy/provider.go +++ /dev/null @@ -1,10 +0,0 @@ -package proxy - -// IPProvider IP提供者接口 -type IPProvider interface { - // GetIPList 获取IP列表 - GetIPList() ([]ProxyIP, error) - - // RefreshIPList 刷新IP列表(可选实现) - RefreshIPList() ([]ProxyIP, error) -} diff --git a/proxy/proxy_client.go b/proxy/proxy_client.go deleted file mode 100644 index cda50b00..00000000 --- a/proxy/proxy_client.go +++ /dev/null @@ -1,47 +0,0 @@ -package proxy - -import ( - "log" - "net/http" - "time" -) - -// --- 便捷函数(直接使用全局管理器) --- - -// GetProxyHTTPClient 获取代理 HTTP 客户端(返回 ProxyClient,包含 ProxyID) -func GetProxyHTTPClient() (*ProxyClient, error) { - return GetGlobalProxyManager().GetProxyClient() -} - -// NewHTTPClient 创建一个新的HTTP客户端(使用全局代理配置) -// 注意:不返回 ProxyID,如需 ProxyID 请使用 GetProxyHTTPClient() -func NewHTTPClient() *http.Client { - client, err := GetGlobalProxyManager().GetProxyClient() - if err != nil { - log.Printf("⚠️ 获取代理客户端失败,使用直连: %v", err) - return &http.Client{Timeout: 30 * time.Second} - } - return client.Client -} - -// NewHTTPClientWithTimeout 创建一个新的HTTP客户端并指定超时时间 -// 注意:不返回 ProxyID,如需 ProxyID 请使用 GetProxyHTTPClient() -func NewHTTPClientWithTimeout(timeout time.Duration) *http.Client { - client, err := GetGlobalProxyManager().GetProxyClient() - if err != nil { - log.Printf("⚠️ 获取代理客户端失败,使用直连: %v", err) - return &http.Client{Timeout: timeout} - } - client.Client.Timeout = timeout - return client.Client -} - -// GetTransport 获取HTTP Transport -func GetTransport() *http.Transport { - client, err := GetGlobalProxyManager().GetProxyClient() - if err != nil { - log.Printf("⚠️ 获取代理客户端失败,使用直连: %v", err) - return &http.Transport{} - } - return client.Client.Transport.(*http.Transport) -} \ No newline at end of file diff --git a/proxy/proxy_manager.go b/proxy/proxy_manager.go deleted file mode 100644 index aaca00e4..00000000 --- a/proxy/proxy_manager.go +++ /dev/null @@ -1,346 +0,0 @@ -package proxy - -import ( - "crypto/tls" - "fmt" - "log" - "math/rand" - "net/http" - "net/url" - "sync" - "time" -) - -// ProxyManager 代理管理器 -type ProxyManager struct { - config *Config - provider IPProvider - - // IP池管理 - ipList []ProxyIP - blacklist map[int]string // ProxyID -> IP - ipBlacklist map[string]int // IP -> 剩余TTL - mutex sync.RWMutex // 读写锁,保证线程安全 - - // 刷新控制 - stopRefresh chan struct{} -} - -var ( - globalProxyManager *ProxyManager - once sync.Once -) - -// InitGlobalProxyManager 初始化全局代理管理器 -func InitGlobalProxyManager(config *Config) error { - var err error - once.Do(func() { - globalProxyManager, err = NewProxyManager(config) - if err == nil && config.Enabled && config.RefreshInterval > 0 { - globalProxyManager.StartAutoRefresh() - } - }) - return err -} - -// GetGlobalProxyManager 获取全局代理管理器 -func GetGlobalProxyManager() *ProxyManager { - if globalProxyManager == nil { - // 如果未初始化,使用默认配置(禁用代理) - _ = InitGlobalProxyManager(&Config{Enabled: false}) - } - return globalProxyManager -} - -// NewProxyManager 创建代理管理器 -func NewProxyManager(config *Config) (*ProxyManager, error) { - if config == nil { - config = &Config{Enabled: false} - } - - // 设置默认值 - if config.Timeout == 0 { - config.Timeout = 30 * time.Second - } - if config.BlacklistTTL == 0 { - config.BlacklistTTL = 5 // 默认 TTL 为 5 次刷新 - } - if config.RefreshInterval == 0 && config.Mode == "brightdata" { - config.RefreshInterval = 30 * time.Minute // 默认 30 分钟刷新一次 - } - - m := &ProxyManager{ - config: config, - blacklist: make(map[int]string), - ipBlacklist: make(map[string]int), - stopRefresh: make(chan struct{}), - } - - // 如果未启用代理,直接返回 - if !config.Enabled { - log.Printf("🌐 HTTP 代理未启用,使用直连") - return m, nil - } - - // 根据模式选择IP提供者 - switch config.Mode { - case "single": - // 单个代理模式 - if config.ProxyURL == "" { - return nil, fmt.Errorf("single模式下必须配置proxy_url") - } - m.provider = NewSingleProxyProvider(config.ProxyURL) - log.Printf("🌐 HTTP 代理已启用 (单代理模式): %s", config.ProxyURL) - - case "pool": - // 代理池模式(固定列表) - if len(config.ProxyList) == 0 { - return nil, fmt.Errorf("pool模式下必须配置proxy_list") - } - m.provider = NewFixedIPProvider(config.ProxyList) - log.Printf("🌐 HTTP 代理已启用 (代理池模式): %d个代理", len(config.ProxyList)) - - case "brightdata": - // Bright Data动态获取模式 - if config.BrightDataEndpoint == "" { - return nil, fmt.Errorf("brightdata模式下必须配置brightdata_endpoint") - } - m.provider = NewBrightDataProvider(config.BrightDataEndpoint, config.BrightDataToken, config.BrightDataZone) - log.Printf("🌐 HTTP 代理已启用 (Bright Data模式): %s", config.BrightDataEndpoint) - - default: - // 默认使用single模式 - if config.ProxyURL == "" { - return nil, fmt.Errorf("未知的proxy模式: %s", config.Mode) - } - m.provider = NewSingleProxyProvider(config.ProxyURL) - log.Printf("🌐 HTTP 代理已启用 (默认模式): %s", config.ProxyURL) - } - - // 初始化IP列表 - if err := m.RefreshIPList(); err != nil { - return nil, fmt.Errorf("初始化IP列表失败: %w", err) - } - - return m, nil -} - -// RefreshIPList 刷新IP列表(线程安全) -func (m *ProxyManager) RefreshIPList() error { - if m.provider == nil { - return nil - } - - ips, err := m.provider.RefreshIPList() - if err != nil { - return err - } - - m.mutex.Lock() - defer m.mutex.Unlock() - - // 清理黑名单,TTL倒计时 - validIPs := make([]ProxyIP, 0, len(ips)) - newBlacklist := make(map[int]string) - - for _, ip := range ips { - if ttl, inBlacklist := m.ipBlacklist[ip.IP]; inBlacklist { - // TTL 倒计时 - m.ipBlacklist[ip.IP] = ttl - 1 - if ttl > 0 { - // 仍在黑名单中,跳过 - continue - } - // TTL 归零,从黑名单移除 - delete(m.ipBlacklist, ip.IP) - log.Printf("✓ 代理IP已从黑名单恢复: %s", ip.IP) - } - validIPs = append(validIPs, ip) - } - - m.ipList = validIPs - m.blacklist = newBlacklist - - log.Printf("✓ 刷新代理IP列表: 总计%d个,黑名单%d个,可用%d个", - len(ips), len(m.ipBlacklist), len(validIPs)) - - return nil -} - -// StartAutoRefresh 启动自动刷新 -func (m *ProxyManager) StartAutoRefresh() { - if m.config.RefreshInterval <= 0 { - return - } - - go func() { - ticker := time.NewTicker(m.config.RefreshInterval) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - if err := m.RefreshIPList(); err != nil { - log.Printf("⚠️ 自动刷新IP列表失败: %v", err) - } - case <-m.stopRefresh: - return - } - } - }() - - log.Printf("✓ 已启动代理IP自动刷新 (间隔: %v)", m.config.RefreshInterval) -} - -// StopAutoRefresh 停止自动刷新 -func (m *ProxyManager) StopAutoRefresh() { - close(m.stopRefresh) -} - -// getRandomProxy 随机获取一个可用代理(线程安全 - 读锁,确保不越界) -func (m *ProxyManager) getRandomProxy() (int, *ProxyIP, error) { - m.mutex.RLock() - defer m.mutex.RUnlock() - - if len(m.ipList) == 0 { - return -1, nil, fmt.Errorf("代理IP列表为空") - } - - // 找到所有未被黑名单的索引 - availableIndices := make([]int, 0, len(m.ipList)) - for i := range m.ipList { - if _, inBlacklist := m.blacklist[i]; !inBlacklist { - availableIndices = append(availableIndices, i) - } - } - - if len(availableIndices) == 0 { - return -1, nil, fmt.Errorf("所有代理IP都在黑名单中") - } - - // 随机选择一个(确保不越界) - randomIdx := availableIndices[rand.Intn(len(availableIndices))] - - // 二次检查,确保索引有效(防御性编程) - if randomIdx < 0 || randomIdx >= len(m.ipList) { - return -1, nil, fmt.Errorf("代理索引越界: %d (总数: %d)", randomIdx, len(m.ipList)) - } - - return randomIdx, &m.ipList[randomIdx], nil -} - -// buildProxyURL 构建代理URL -func (m *ProxyManager) buildProxyURL(ip *ProxyIP) string { - if m.config.ProxyHost != "" && m.config.ProxyUser != "" { - // 使用配置的代理主机和认证信息 - user := m.config.ProxyUser - if m.config.ProxyUser != "" && ip.IP != "" { - // 支持%s占位符替换IP - user = fmt.Sprintf(m.config.ProxyUser, ip.IP) - } - - protocol := ip.Protocol - if protocol == "" { - protocol = "http" - } - - if m.config.ProxyPassword != "" { - return fmt.Sprintf("%s://%s:%s@%s", protocol, user, m.config.ProxyPassword, m.config.ProxyHost) - } - return fmt.Sprintf("%s://%s@%s", protocol, user, m.config.ProxyHost) - } - - // 直接使用IP信息 - return ip.IP -} - -// GetProxyClient 获取代理客户端(线程安全) -func (m *ProxyManager) GetProxyClient() (*ProxyClient, error) { - if !m.config.Enabled { - // 未启用代理,返回普通HTTP客户端 - return &ProxyClient{ - ProxyID: -1, // -1 表示未使用代理 - IP: "direct", - Client: &http.Client{ - Timeout: m.config.Timeout, - }, - }, nil - } - - // 获取随机代理(使用读锁,确保不越界) - proxyID, proxyIP, err := m.getRandomProxy() - if err != nil { - return nil, err - } - - // 构建代理URL - proxyURLStr := m.buildProxyURL(proxyIP) - proxyURL, err := url.Parse(proxyURLStr) - if err != nil { - return nil, fmt.Errorf("解析代理URL失败: %w", err) - } - - // 创建Transport - transport := &http.Transport{ - Proxy: http.ProxyURL(proxyURL), - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: false, - }, - MaxIdleConns: 100, - MaxIdleConnsPerHost: 10, - IdleConnTimeout: 90 * time.Second, - } - - return &ProxyClient{ - ProxyID: proxyID, - IP: proxyIP.IP, - Client: &http.Client{ - Transport: transport, - Timeout: m.config.Timeout, - }, - }, nil -} - -// AddBlacklist 将代理IP添加到黑名单(线程安全 - 写锁) -func (m *ProxyManager) AddBlacklist(proxyID int) { - m.mutex.Lock() - defer m.mutex.Unlock() - - // 检查 proxyID 有效性,防止越界 - if proxyID < 0 || proxyID >= len(m.ipList) { - log.Printf("⚠️ 无效的 ProxyID: %d (有效范围: 0-%d)", proxyID, len(m.ipList)-1) - return - } - - ip := m.ipList[proxyID].IP - m.blacklist[proxyID] = ip - m.ipBlacklist[ip] = m.config.BlacklistTTL - - log.Printf("⚠️ 代理IP已加入黑名单: %s (ProxyID: %d, TTL: %d)", ip, proxyID, m.config.BlacklistTTL) -} - -// GetBlacklistStatus 获取黑名单状态(线程安全 - 读锁) -func (m *ProxyManager) GetBlacklistStatus() (total int, blacklisted int, available int) { - m.mutex.RLock() - defer m.mutex.RUnlock() - - total = len(m.ipList) - blacklisted = len(m.ipBlacklist) - available = total - len(m.blacklist) - return -} - -// IsEnabled 检查代理是否启用 -func IsEnabled() bool { - return GetGlobalProxyManager().config.Enabled -} - -// RefreshIPList 刷新全局代理IP列表 -func RefreshIPList() error { - return GetGlobalProxyManager().RefreshIPList() -} - -// AddBlacklist 将代理IP添加到全局黑名单 -func AddBlacklist(proxyID int) { - GetGlobalProxyManager().AddBlacklist(proxyID) -} diff --git a/proxy/single_provider.go b/proxy/single_provider.go deleted file mode 100644 index bbea9fce..00000000 --- a/proxy/single_provider.go +++ /dev/null @@ -1,19 +0,0 @@ -package proxy - -// SingleProxyProvider 单个代理提供者(不使用IP池) -type SingleProxyProvider struct { - proxyURL string -} - -// NewSingleProxyProvider 创建单个代理提供者 -func NewSingleProxyProvider(proxyURL string) *SingleProxyProvider { - return &SingleProxyProvider{proxyURL: proxyURL} -} - -func (p *SingleProxyProvider) GetIPList() ([]ProxyIP, error) { - return []ProxyIP{{IP: p.proxyURL}}, nil -} - -func (p *SingleProxyProvider) RefreshIPList() ([]ProxyIP, error) { - return p.GetIPList() -} diff --git a/proxy/types.go b/proxy/types.go deleted file mode 100644 index 89678c86..00000000 --- a/proxy/types.go +++ /dev/null @@ -1,40 +0,0 @@ -package proxy - -import ( - "net/http" - "time" -) - -// ProxyIP 代理IP信息 -type ProxyIP struct { - IP string `json:"ip"` // IP地址 - Port string `json:"port"` // 端口(可选) - Username string `json:"username"` // 用户名(可选) - Password string `json:"password"` // 密码(可选) - Protocol string `json:"protocol"` // 协议: http, https, socks5 - Ext map[string]interface{} `json:"ext"` // 扩展信息 -} - -// ProxyClient 代理客户端 -type ProxyClient struct { - ProxyID int // IP池中的代理ID(索引) - IP string // 使用的IP地址 - *http.Client // HTTP客户端 -} - -// Config 代理配置 -type Config struct { - Enabled bool // 是否启用代理 - Mode string // 模式: "single", "pool", "brightdata" - Timeout time.Duration // 超时时间 - ProxyURL string // 单个代理地址 (single模式) - ProxyList []string // 代理列表 (pool模式) - BrightDataEndpoint string // Bright Data接口地址 (brightdata模式) - BrightDataToken string // Bright Data访问令牌 (brightdata模式) - BrightDataZone string // Bright Data区域 (brightdata模式) - ProxyHost string // 代理主机 - ProxyUser string // 代理用户名模板(支持%s占位符) - ProxyPassword string // 代理密码 - RefreshInterval time.Duration // IP列表刷新间隔 - BlacklistTTL int // 黑名单IP的TTL(刷新次数) -} diff --git a/scripts/generate_beta_code.sh b/scripts/generate_beta_code.sh deleted file mode 100755 index 228d256f..00000000 --- a/scripts/generate_beta_code.sh +++ /dev/null @@ -1,228 +0,0 @@ -#!/bin/bash - -# Fail fast and normalize working directory -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -cd "$ROOT_DIR" - -# 内测码生成脚本 -# 生成6位不重复的内测码并写入 beta_codes.txt - -BETA_CODES_FILE="beta_codes.txt" -COUNT=1 -LIST_ONLY=false -CODE_LENGTH=6 - -# 字符集(避免易混淆字符:0/O, 1/I/l) -CHARSET="23456789abcdefghjkmnpqrstuvwxyz" - -# 显示帮助信息 -show_help() { - cat << EOF -用法: $0 [选项] - -选项: - -c COUNT 生成内测码数量 (默认: 1) - -l 列出现有内测码 - -f FILE 内测码文件路径 (默认: beta_codes.txt) - -h 显示此帮助信息 - -示例: - $0 -c 10 # 生成10个内测码 - $0 -l # 列出现有内测码 - $0 -f custom.txt -c 5 # 在自定义文件中生成5个内测码 -EOF -} - -# 生成随机内测码 -generate_beta_code() { - local length="$1" - local charset="$2" - local code="" - - for ((i=0; i/dev/null | tr -d ' \t' | grep -v '^#' || true - fi -} - -# 检查内测码是否已存在 -code_exists() { - local code="$1" - local file="$2" - if [ -f "$file" ]; then - grep -Fxq "$code" "$file" 2>/dev/null - else - return 1 - fi -} - -# 添加内测码到文件 -add_code_to_file() { - local code="$1" - local file="$2" - echo "$code" >> "$file" -} - -# 验证内测码格式 -validate_code() { - local code="$1" - # 检查长度 - if [ ${#code} -ne $CODE_LENGTH ]; then - return 1 - fi - # 检查字符是否都在允许的字符集中 - if [[ ! "$code" =~ ^[$CHARSET]+$ ]]; then - return 1 - fi - return 0 -} - -# 去重并排序内测码 -dedupe_and_sort_codes() { - local file="$1" - if [ -f "$file" ]; then - # 过滤空行和注释,去重并排序 - grep -v '^$' "$file" | grep -v '^#' | sort -u > "${file}.tmp" && mv "${file}.tmp" "$file" - fi -} - -# 解析命令行参数 -while getopts "c:lf:h" opt; do - case $opt in - c) - COUNT="$OPTARG" - if ! [[ "$COUNT" =~ ^[0-9]+$ ]] || [ "$COUNT" -lt 1 ]; then - echo "错误: count 必须是正整数" >&2 - exit 1 - fi - ;; - l) - LIST_ONLY=true - ;; - f) - BETA_CODES_FILE="$OPTARG" - ;; - h) - show_help - exit 0 - ;; - \?) - echo "无效选项: -$OPTARG" >&2 - echo "使用 -h 查看帮助信息" >&2 - exit 1 - ;; - esac -done - -# 如果是列出现有内测码 -if [ "$LIST_ONLY" = true ]; then - if [ -f "$BETA_CODES_FILE" ]; then - existing_codes=$(read_existing_codes "$BETA_CODES_FILE") - if [ -z "$existing_codes" ]; then - echo "内测码列表为空" - else - count=$(echo "$existing_codes" | wc -l | tr -d ' ') - echo "当前内测码 ($count 个):" - echo "$existing_codes" | nl -w3 -s'. ' - fi - else - echo "内测码文件不存在: $BETA_CODES_FILE" - fi - exit 0 -fi - -# 读取现有内测码 -existing_codes=$(read_existing_codes "$BETA_CODES_FILE") - -# 生成新内测码 -new_codes=() -max_attempts=1000 # 防止无限循环 - -echo "正在生成 $COUNT 个内测码..." - -for ((i=1; i<=COUNT; i++)); do - attempts=0 - while [ $attempts -lt $max_attempts ]; do - code=$(generate_beta_code $CODE_LENGTH "$CHARSET") - - # 验证格式 - if ! validate_code "$code"; then - ((attempts++)) - continue - fi - - # 检查是否已存在 - if code_exists "$code" "$BETA_CODES_FILE"; then - ((attempts++)) - continue - fi - - # 检查是否与本次生成的重复 - duplicate=false - for existing_code in "${new_codes[@]}"; do - if [ "$code" = "$existing_code" ]; then - duplicate=true - break - fi - done - - if [ "$duplicate" = false ]; then - new_codes+=("$code") - break - fi - - ((attempts++)) - done - - if [ $attempts -eq $max_attempts ]; then - echo "警告: 生成第 $i 个内测码时达到最大尝试次数,可能字符空间不足" >&2 - break - fi -done - -# 检查是否成功生成了内测码 -if [ ${#new_codes[@]} -eq 0 ]; then - echo "未能生成任何新的内测码" - exit 1 -fi - -# 添加到文件 -for code in "${new_codes[@]}"; do - add_code_to_file "$code" "$BETA_CODES_FILE" -done - -# 去重并排序 -dedupe_and_sort_codes "$BETA_CODES_FILE" - -echo "成功生成 ${#new_codes[@]} 个内测码:" -printf ' %s\n' "${new_codes[@]}" -echo -echo "内测码文件: $BETA_CODES_FILE" - -# 显示当前总数 -if [ -f "$BETA_CODES_FILE" ]; then - total_count=$(read_existing_codes "$BETA_CODES_FILE" | wc -l | tr -d ' ') - echo "当前内测码总计: $total_count 个" -fi - -# 显示文件头部信息(如果是新文件) -if [ ! -s "$BETA_CODES_FILE" ] || [ $(wc -l < "$BETA_CODES_FILE") -eq ${#new_codes[@]} ]; then - echo - echo "内测码规则:" - echo "- 长度: $CODE_LENGTH 位" - echo "- 字符集: 数字 2-9, 小写字母 a-z (排除 0,1,i,l,o 避免混淆)" - echo "- 每个内测码唯一且不重复" -fi diff --git a/scripts/generate_rsa_keys/main.go b/scripts/generate_rsa_keys/main.go deleted file mode 100644 index c3f642e2..00000000 --- a/scripts/generate_rsa_keys/main.go +++ /dev/null @@ -1,76 +0,0 @@ -package main - -import ( - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "fmt" - "io/ioutil" - "os" - "path/filepath" -) - -func main() { - keysDir := "keys" - if err := os.MkdirAll(keysDir, 0700); err != nil { - fmt.Printf("创建keys目录失败: %v\n", err) - return - } - - privateKeyPath := filepath.Join(keysDir, "rsa_private.key") - publicKeyPath := filepath.Join(keysDir, "rsa_private.key.pub") - - if _, err := os.Stat(privateKeyPath); err == nil { - fmt.Println("RSA密钥对已存在:") - fmt.Printf(" 私钥: %s\n", privateKeyPath) - fmt.Printf(" 公钥: %s\n", publicKeyPath) - - publicKeyPEM, err := ioutil.ReadFile(publicKeyPath) - if err == nil { - fmt.Println("\n公钥内容:") - fmt.Println(string(publicKeyPEM)) - } - return - } - - fmt.Println("生成新的RSA密钥对...") - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - fmt.Printf("生成RSA密钥失败: %v\n", err) - return - } - - privateKeyPEM := pem.EncodeToMemory(&pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(privateKey), - }) - - if err := ioutil.WriteFile(privateKeyPath, privateKeyPEM, 0600); err != nil { - fmt.Printf("保存私钥失败: %v\n", err) - return - } - - publicKeyDER, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) - if err != nil { - fmt.Printf("编码公钥失败: %v\n", err) - return - } - - publicKeyPEM := pem.EncodeToMemory(&pem.Block{ - Type: "PUBLIC KEY", - Bytes: publicKeyDER, - }) - - if err := ioutil.WriteFile(publicKeyPath, publicKeyPEM, 0644); err != nil { - fmt.Printf("保存公钥失败: %v\n", err) - return - } - - fmt.Println("✓ RSA密钥对生成成功!") - fmt.Printf(" 私钥: %s\n", privateKeyPath) - fmt.Printf(" 公钥: %s\n", publicKeyPath) - fmt.Println("\n公钥内容(可用于前端配置):") - fmt.Println(string(publicKeyPEM)) - fmt.Println("\n注意: 请妥善保管私钥文件,不要提交到版本控制系统中!") -} diff --git a/scripts/import_beta_codes.sh b/scripts/import_beta_codes.sh deleted file mode 100755 index dab961ae..00000000 --- a/scripts/import_beta_codes.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -cd "$ROOT_DIR" - -echo "🎟️ 导入 beta_codes.txt 到 PostgreSQL" - -if [ ! -f "beta_codes.txt" ]; then - echo "❌ 找不到 beta_codes.txt 文件" - exit 1 -fi - -if command -v "docker-compose" &> /dev/null; then - DOCKER_CMD="docker-compose" -elif command -v "docker" &> /dev/null && docker compose version &> /dev/null; then - DOCKER_CMD="docker compose" -else - echo "❌ 错误:找不到 docker-compose 或 docker compose 命令" - exit 1 -fi - -ENV_FILE=".env" -if [ -f "$ENV_FILE" ]; then - echo "📁 加载 .env 配置..." - set -a - # shellcheck disable=SC1090 - source "$ENV_FILE" - set +a -else - echo "⚠️ 未找到 .env 文件,使用默认数据库配置" -fi - -POSTGRES_HOST=${POSTGRES_HOST:-postgres} -POSTGRES_PORT=${POSTGRES_PORT:-5432} -POSTGRES_DB=${POSTGRES_DB:-nofx} -POSTGRES_USER=${POSTGRES_USER:-nofx} -POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-} -POSTGRES_SERVICE=${POSTGRES_SERVICE:-postgres} -POSTGRES_CONTAINER_NAME=${POSTGRES_CONTAINER_NAME:-nofx-postgres} - -POSTGRES_CONTAINER=$($DOCKER_CMD ps -q "$POSTGRES_SERVICE" 2>/dev/null || true) -if [ -z "$POSTGRES_CONTAINER" ]; then - POSTGRES_CONTAINER=$(docker ps -q --filter "name=$POSTGRES_CONTAINER_NAME" | head -n 1) -fi - -if [ -z "$POSTGRES_CONTAINER" ]; then - echo "❌ 找不到 PostgreSQL 容器 (${POSTGRES_SERVICE}/${POSTGRES_CONTAINER_NAME})" - echo "💡 请确认数据库服务已启动" - exit 1 -fi - -PG_ENV_ARGS=() -if [ -n "$POSTGRES_PASSWORD" ]; then - PG_ENV_ARGS=(--env "PGPASSWORD=$POSTGRES_PASSWORD") -fi - -SQL_PAYLOAD=$(python3 - <<'PY' -from pathlib import Path - -codes = [] -for line in Path('beta_codes.txt').read_text(encoding='utf-8').splitlines(): - code = line.strip() - if code and not code.startswith('#'): - codes.append(f"('{code}')") - -if codes: - values = ",\n".join(codes) - print(f"INSERT INTO beta_codes (code) VALUES\n{values}\nON CONFLICT (code) DO NOTHING;") -PY -) - -if [ -z "$SQL_PAYLOAD" ]; then - echo "⚠️ beta_codes.txt 中没有有效的内测码,已跳过导入" - exit 0 -fi - -TOTAL_CODES=$(grep -vc '^\s*$' beta_codes.txt || true) -echo "📊 检测到 $TOTAL_CODES 条内测码记录" - -echo "🔄 导入到数据库..." -printf '%s\n' "$SQL_PAYLOAD" | docker exec -i "${PG_ENV_ARGS[@]}" "$POSTGRES_CONTAINER" \ - psql -v ON_ERROR_STOP=1 --pset pager=off -U "$POSTGRES_USER" -d "$POSTGRES_DB" - -echo "✅ 导入完成(重复的已跳过)" diff --git a/scripts/import_default_patch.sh b/scripts/import_default_patch.sh deleted file mode 100755 index ef971edd..00000000 --- a/scripts/import_default_patch.sh +++ /dev/null @@ -1,160 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -echo "🔧 同步默认用户与基础配置" -echo "===============================" - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -cd "$ROOT_DIR" - -# 检测 Docker Compose 命令 -if command -v docker-compose &> /dev/null; then - DOCKER_COMPOSE_CMD="docker-compose" -elif docker compose version &> /dev/null; then - DOCKER_COMPOSE_CMD="docker compose" -else - echo "❌ 无法找到 docker-compose 或 docker compose 命令" - exit 1 -fi - -echo "📋 使用命令: $DOCKER_COMPOSE_CMD" - -# 加载 .env 配置 -ENV_FILE=".env" -if [ -f "$ENV_FILE" ]; then - echo "📁 加载 .env ..." - set -a - # shellcheck disable=SC1090 - source "$ENV_FILE" - set +a -else - echo "⚠️ 未找到 .env,使用默认数据库配置" -fi - -POSTGRES_HOST=${POSTGRES_HOST:-postgres} -POSTGRES_PORT=${POSTGRES_PORT:-5432} -POSTGRES_DB=${POSTGRES_DB:-nofx} -POSTGRES_USER=${POSTGRES_USER:-nofx} -POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-} -POSTGRES_SERVICE=${POSTGRES_SERVICE:-postgres} -POSTGRES_CONTAINER_NAME=${POSTGRES_CONTAINER_NAME:-nofx-postgres} - -# 查找 PostgreSQL 容器 -POSTGRES_CONTAINER=$($DOCKER_COMPOSE_CMD ps -q "$POSTGRES_SERVICE" 2>/dev/null || true) -if [ -z "$POSTGRES_CONTAINER" ]; then - POSTGRES_CONTAINER=$(docker ps -q --filter "name=$POSTGRES_CONTAINER_NAME" | head -n 1) -fi - -if [ -z "$POSTGRES_CONTAINER" ]; then - echo "❌ 未找到 PostgreSQL 容器 (${POSTGRES_SERVICE}/${POSTGRES_CONTAINER_NAME})" - echo "💡 请先启动数据库容器: $DOCKER_COMPOSE_CMD up -d postgres" - exit 1 -fi - -PG_ENV_ARGS=() -if [ -n "$POSTGRES_PASSWORD" ]; then - PG_ENV_ARGS=(-e "PGPASSWORD=$POSTGRES_PASSWORD") -fi - -echo "🔌 检查数据库连接..." -if ! docker exec "${PG_ENV_ARGS[@]}" "$POSTGRES_CONTAINER" pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" > /dev/null 2>&1; then - echo "❌ 无法连接到 PostgreSQL,请确认容器和凭据" - exit 1 -fi - -echo -read -p "确认写入默认账号和基础配置? (y/N): " confirm -if [[ $confirm != [yY] ]]; then - echo "ℹ️ 已取消操作" - exit 0 -fi - -echo "🚀 执行初始化 SQL..." -if docker exec -i "${PG_ENV_ARGS[@]}" "$POSTGRES_CONTAINER" \ - psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" <<'SQL' --- 确保 traders 表存在 custom_coins 字段 -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_name = 'traders' AND column_name = 'custom_coins' - ) THEN - ALTER TABLE traders ADD COLUMN custom_coins TEXT DEFAULT ''; - END IF; -END -$$; - --- 创建 default 用户 -INSERT INTO users (id, email, password_hash, otp_secret, otp_verified, created_at, updated_at) -VALUES ('default', 'default@localhost', '', '', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) -ON CONFLICT (id) DO UPDATE - SET email = EXCLUDED.email, - updated_at = CURRENT_TIMESTAMP; - --- 默认 AI 模型配置 -INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, custom_model_name, created_at, updated_at) VALUES -('deepseek', 'default', 'DeepSeek', 'deepseek', false, '', '', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), -('qwen', 'default', 'Qwen', 'qwen', false, '', '', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) -ON CONFLICT (id) DO UPDATE - SET user_id = EXCLUDED.user_id, - name = EXCLUDED.name, - provider = EXCLUDED.provider, - enabled = EXCLUDED.enabled, - api_key = EXCLUDED.api_key, - custom_api_url = EXCLUDED.custom_api_url, - custom_model_name = EXCLUDED.custom_model_name, - updated_at = CURRENT_TIMESTAMP; - --- 默认交易所配置 -INSERT INTO exchanges (id, user_id, name, type, enabled, api_key, secret_key, testnet, - hyperliquid_wallet_addr, aster_user, aster_signer, aster_private_key, - created_at, updated_at) VALUES -('binance', 'default', 'Binance Futures', 'binance', false, '', '', false, '', '', '', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), -('hyperliquid', 'default', 'Hyperliquid', 'hyperliquid', false, '', '', false, '', '', '', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), -('aster', 'default', 'Aster DEX', 'aster', false, '', '', false, '', '', '', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) -ON CONFLICT (id, user_id) DO UPDATE - SET name = EXCLUDED.name, - type = EXCLUDED.type, - enabled = EXCLUDED.enabled, - api_key = EXCLUDED.api_key, - secret_key = EXCLUDED.secret_key, - testnet = EXCLUDED.testnet, - hyperliquid_wallet_addr = EXCLUDED.hyperliquid_wallet_addr, - aster_user = EXCLUDED.aster_user, - aster_signer = EXCLUDED.aster_signer, - aster_private_key = EXCLUDED.aster_private_key, - updated_at = CURRENT_TIMESTAMP; - --- 默认系统配置(不存在时写入) -INSERT INTO system_config (key, value) VALUES -('beta_mode', 'false'), -('api_server_port', '8080'), -('use_default_coins', 'true'), -('default_coins', '["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]'), -('max_daily_loss', '10.0'), -('max_drawdown', '20.0'), -('stop_trading_minutes', '60'), -('btc_eth_leverage', '5'), -('altcoin_leverage', '5'), -('jwt_secret', '') -ON CONFLICT (key) DO NOTHING; - --- 输出校验信息 -SELECT 'default_user' AS item, COUNT(*) AS count FROM users WHERE id = 'default' -UNION ALL -SELECT 'default_ai_models', COUNT(*) FROM ai_models WHERE user_id = 'default' -UNION ALL -SELECT 'default_exchanges', COUNT(*) FROM exchanges WHERE user_id = 'default'; -SQL -then - echo - echo "✅ 默认数据写入完成" -else - echo - echo "❌ 数据写入失败" - exit 1 -fi - -echo "🎉 操作完成" diff --git a/scripts/migrate_sensitive_data/main.go b/scripts/migrate_sensitive_data/main.go deleted file mode 100644 index f5db9a3b..00000000 --- a/scripts/migrate_sensitive_data/main.go +++ /dev/null @@ -1,367 +0,0 @@ -package main - -import ( - "bufio" - "database/sql" - "flag" - "fmt" - "log" - "nofx/crypto" - "os" - "strings" - "time" - - _ "github.com/lib/pq" -) - -func main() { - privateKeyPath := flag.String("key", "keys/rsa_private.key", "RSA 私钥路径") - dryRun := flag.Bool("dry-run", false, "仅检查需要迁移的数据,不写入数据库") - flag.Parse() - - // 尝试加载 .env 文件(从项目根目录运行时) - envPaths := []string{ - ".env", // 项目根目录 - } - envLoaded := false - for _, envPath := range envPaths { - if err := loadEnvFile(envPath); err == nil { - log.Printf("成功加载 .env 文件: %s", envPath) - envLoaded = true - break - } - } - if !envLoaded { - log.Printf("警告: 未找到 .env 文件,请确保在项目根目录存在 .env 文件") - log.Printf("尝试的路径: %v", envPaths) - } - - // 确保环境变量已设置 - if os.Getenv("DATA_ENCRYPTION_KEY") == "" { - log.Fatalf("迁移失败: DATA_ENCRYPTION_KEY 环境变量未设置") - } - - if err := run(*privateKeyPath, *dryRun); err != nil { - log.Fatalf("迁移失败: %v", err) - } -} - -func run(privateKeyPath string, dryRun bool) error { - log.SetFlags(0) - - // 尝试多个可能的私钥路径(从项目根目录运行时) - keyPaths := []string{ - privateKeyPath, // 用户指定的路径 - "keys/rsa_private.key", // 项目根目录的 keys 文件夹 - } - - var finalKeyPath string - for _, path := range keyPaths { - if _, err := os.Stat(path); err == nil { - finalKeyPath = path - log.Printf("找到私钥文件: %s", path) - break - } - } - - if finalKeyPath == "" { - finalKeyPath = privateKeyPath // 使用默认路径,让 crypto 服务生成新密钥 - log.Printf("警告: 私钥文件不存在,将使用路径: %s, 系统将尝试生成新密钥", finalKeyPath) - } - - cryptoService, err := crypto.NewCryptoService(finalKeyPath) - if err != nil { - return fmt.Errorf("初始化加密服务失败: %w", err) - } - - db, err := openPostgres() - if err != nil { - return fmt.Errorf("连接数据库失败: %w", err) - } - defer db.Close() - - log.Printf("开始迁移 AI 模型密钥 (dry-run=%v)", dryRun) - if err := migrateAIModels(db, cryptoService, dryRun); err != nil { - return fmt.Errorf("迁移 AI 模型失败: %w", err) - } - - log.Printf("开始迁移交易所密钥 (dry-run=%v)", dryRun) - if err := migrateExchanges(db, cryptoService, dryRun); err != nil { - return fmt.Errorf("迁移交易所失败: %w", err) - } - - log.Printf("✓ 敏感数据迁移完成") - return nil -} - -func openPostgres() (*sql.DB, error) { - host := getEnv("POSTGRES_HOST", "localhost") - // 如果是 Docker 服务名,替换为 localhost - if host == "postgres" { - host = "localhost" - } - port := getEnv("POSTGRES_PORT", "5432") - dbname := getEnv("POSTGRES_DB", "nofx") - user := getEnv("POSTGRES_USER", "nofx") - password := getEnv("POSTGRES_PASSWORD", "nofx123456") - - dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", - host, port, user, password, dbname) - - db, err := sql.Open("postgres", dsn) - if err != nil { - return nil, err - } - - db.SetMaxOpenConns(5) - db.SetMaxIdleConns(2) - db.SetConnMaxLifetime(5 * time.Minute) - - if err := db.Ping(); err != nil { - db.Close() - return nil, err - } - - return db, nil -} - -func migrateAIModels(db *sql.DB, cryptoService *crypto.CryptoService, dryRun bool) error { - type record struct { - ID string - UserID string - APIKey string - } - - rows, err := db.Query(` - SELECT id, user_id, COALESCE(api_key, '') - FROM ai_models - WHERE COALESCE(deleted, FALSE) = FALSE - `) - if err != nil { - return err - } - defer rows.Close() - - var records []record - for rows.Next() { - var r record - if err := rows.Scan(&r.ID, &r.UserID, &r.APIKey); err != nil { - return err - } - records = append(records, r) - } - if err := rows.Err(); err != nil { - return err - } - - var updated int - for _, r := range records { - if r.APIKey == "" || cryptoService.IsEncryptedStorageValue(r.APIKey) { - continue - } - - encrypted, err := cryptoService.EncryptForStorage(r.APIKey, r.UserID, r.ID, "api_key") - if err != nil { - return fmt.Errorf("加密 AI 模型 %s (%s) 失败: %w", r.ID, r.UserID, err) - } - - updated++ - if dryRun { - log.Printf("[DRY-RUN] AI 模型 %s (%s) 将被加密", r.ID, r.UserID) - continue - } - - if _, err := db.Exec(` - UPDATE ai_models - SET api_key = $1, updated_at = CURRENT_TIMESTAMP - WHERE id = $2 AND user_id = $3 - `, encrypted, r.ID, r.UserID); err != nil { - return fmt.Errorf("更新 AI 模型 %s (%s) 失败: %w", r.ID, r.UserID, err) - } - } - - log.Printf("AI 模型处理完成,需更新 %d 条记录", updated) - return nil -} - -func migrateExchanges(db *sql.DB, cryptoService *crypto.CryptoService, dryRun bool) error { - type record struct { - ID string - UserID string - APIKey string - SecretKey string - HyperliquidWallet string - AsterUser string - AsterSigner string - AsterPrivateKey string - } - - rows, err := db.Query(` - SELECT id, user_id, - COALESCE(api_key, '') AS api_key, - COALESCE(secret_key, '') AS secret_key, - COALESCE(hyperliquid_wallet_addr, '') AS hyperliquid_wallet_addr, - COALESCE(aster_user, '') AS aster_user, - COALESCE(aster_signer, '') AS aster_signer, - COALESCE(aster_private_key, '') AS aster_private_key - FROM exchanges - WHERE COALESCE(deleted, FALSE) = FALSE - `) - if err != nil { - return err - } - defer rows.Close() - - var records []record - for rows.Next() { - var r record - if err := rows.Scan( - &r.ID, &r.UserID, - &r.APIKey, &r.SecretKey, - &r.HyperliquidWallet, - &r.AsterUser, &r.AsterSigner, &r.AsterPrivateKey, - ); err != nil { - return err - } - records = append(records, r) - } - if err := rows.Err(); err != nil { - return err - } - - var updated int - for _, r := range records { - newAPIKey := r.APIKey - newSecretKey := r.SecretKey - newHyper := r.HyperliquidWallet - newAsterUser := r.AsterUser - newAsterSigner := r.AsterSigner - newAsterPrivate := r.AsterPrivateKey - - changed := false - - if r.APIKey != "" && !cryptoService.IsEncryptedStorageValue(r.APIKey) { - enc, err := cryptoService.EncryptForStorage(r.APIKey, r.UserID, r.ID, "api_key") - if err != nil { - return fmt.Errorf("加密交易所 API Key 失败: %s (%s): %w", r.ID, r.UserID, err) - } - newAPIKey = enc - changed = true - } - if r.SecretKey != "" && !cryptoService.IsEncryptedStorageValue(r.SecretKey) { - enc, err := cryptoService.EncryptForStorage(r.SecretKey, r.UserID, r.ID, "secret_key") - if err != nil { - return fmt.Errorf("加密交易所 Secret Key 失败: %s (%s): %w", r.ID, r.UserID, err) - } - newSecretKey = enc - changed = true - } - if r.HyperliquidWallet != "" && !cryptoService.IsEncryptedStorageValue(r.HyperliquidWallet) { - enc, err := cryptoService.EncryptForStorage(r.HyperliquidWallet, r.UserID, r.ID, "hyperliquid_wallet_addr") - if err != nil { - return fmt.Errorf("加密 Hyperliquid 地址失败: %s (%s): %w", r.ID, r.UserID, err) - } - newHyper = enc - changed = true - } - if r.AsterUser != "" && !cryptoService.IsEncryptedStorageValue(r.AsterUser) { - enc, err := cryptoService.EncryptForStorage(r.AsterUser, r.UserID, r.ID, "aster_user") - if err != nil { - return fmt.Errorf("加密 Aster 用户失败: %s (%s): %w", r.ID, r.UserID, err) - } - newAsterUser = enc - changed = true - } - if r.AsterSigner != "" && !cryptoService.IsEncryptedStorageValue(r.AsterSigner) { - enc, err := cryptoService.EncryptForStorage(r.AsterSigner, r.UserID, r.ID, "aster_signer") - if err != nil { - return fmt.Errorf("加密 Aster Signer 失败: %s (%s): %w", r.ID, r.UserID, err) - } - newAsterSigner = enc - changed = true - } - if r.AsterPrivateKey != "" && !cryptoService.IsEncryptedStorageValue(r.AsterPrivateKey) { - enc, err := cryptoService.EncryptForStorage(r.AsterPrivateKey, r.UserID, r.ID, "aster_private_key") - if err != nil { - return fmt.Errorf("加密 Aster 私钥失败: %s (%s): %w", r.ID, r.UserID, err) - } - newAsterPrivate = enc - changed = true - } - - if !changed { - continue - } - - updated++ - if dryRun { - log.Printf("[DRY-RUN] 交易所 %s (%s) 将被加密", r.ID, r.UserID) - continue - } - - if _, err := db.Exec(` - UPDATE exchanges - SET api_key = $1, - secret_key = $2, - hyperliquid_wallet_addr = $3, - aster_user = $4, - aster_signer = $5, - aster_private_key = $6, - updated_at = CURRENT_TIMESTAMP - WHERE id = $7 AND user_id = $8 - `, newAPIKey, newSecretKey, newHyper, newAsterUser, newAsterSigner, newAsterPrivate, r.ID, r.UserID); err != nil { - return fmt.Errorf("更新交易所 %s (%s) 失败: %w", r.ID, r.UserID, err) - } - } - - log.Printf("交易所处理完成,需更新 %d 条记录", updated) - return nil -} - -func getEnv(key, fallback string) string { - if val := os.Getenv(key); val != "" { - return val - } - return fallback -} - -func loadEnvFile(filename string) error { - // 检查文件是否存在 - if _, err := os.Stat(filename); os.IsNotExist(err) { - return fmt.Errorf("文件不存在: %s", filename) - } - - // 打开文件 - file, err := os.Open(filename) - if err != nil { - return fmt.Errorf("无法打开文件: %w", err) - } - defer file.Close() - - // 逐行读取 - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - - // 跳过空行和注释行 - if line == "" || strings.HasPrefix(line, "#") { - continue - } - - // 解析 KEY=VALUE 格式 - parts := strings.SplitN(line, "=", 2) - if len(parts) != 2 { - continue - } - - key := strings.TrimSpace(parts[0]) - value := strings.TrimSpace(parts[1]) - - // 只有当环境变量不存在时才设置 - if os.Getenv(key) == "" { - os.Setenv(key, value) - } - } - - return scanner.Err() -} diff --git a/scripts/view_pg_data.sh b/scripts/view_pg_data.sh deleted file mode 100755 index d6c1f06d..00000000 --- a/scripts/view_pg_data.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -# 保证从仓库根目录运行 -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -cd "$ROOT_DIR" - -# PostgreSQL数据查看工具 -echo "🔍 PostgreSQL 数据查看工具" -echo "==========================" - -# 检测Docker Compose命令 -DOCKER_COMPOSE_CMD="" -if command -v "docker-compose" &> /dev/null; then - DOCKER_COMPOSE_CMD="docker-compose" -elif command -v "docker" &> /dev/null && docker compose version &> /dev/null; then - DOCKER_COMPOSE_CMD="docker compose" -else - echo "❌ 错误:找不到 docker-compose 或 docker compose 命令" - exit 1 -fi - -# 加载数据库配置 -ENV_FILE=".env" -if [ -f "$ENV_FILE" ]; then - echo "📁 加载 .env 配置..." - set -a - # shellcheck disable=SC1090 - source "$ENV_FILE" - set +a -else - echo "⚠️ 未找到 .env 文件,使用默认数据库配置" -fi - -POSTGRES_HOST=${POSTGRES_HOST:-postgres} -POSTGRES_PORT=${POSTGRES_PORT:-5432} -POSTGRES_DB=${POSTGRES_DB:-nofx} -POSTGRES_USER=${POSTGRES_USER:-nofx} -POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-} -POSTGRES_SERVICE=${POSTGRES_SERVICE:-postgres} -POSTGRES_CONTAINER_NAME=${POSTGRES_CONTAINER_NAME:-nofx-postgres} - -# 获取 PostgreSQL 容器 ID -POSTGRES_CONTAINER=$($DOCKER_COMPOSE_CMD ps -q "$POSTGRES_SERVICE" 2>/dev/null || true) -if [ -z "$POSTGRES_CONTAINER" ]; then - POSTGRES_CONTAINER=$(docker ps -q --filter "name=$POSTGRES_CONTAINER_NAME" | head -n 1) -fi - -if [ -z "$POSTGRES_CONTAINER" ]; then - echo "❌ 找不到 PostgreSQL 容器 (${POSTGRES_SERVICE}/${POSTGRES_CONTAINER_NAME})" - echo "💡 请确认数据库服务已启动" - exit 1 -fi - -PG_ENV_ARGS=() -if [ -n "$POSTGRES_PASSWORD" ]; then - PG_ENV_ARGS=(--env "PGPASSWORD=$POSTGRES_PASSWORD") -fi - -run_psql() { - local sql="$1" - docker exec -i "${PG_ENV_ARGS[@]}" "$POSTGRES_CONTAINER" \ - psql -v ON_ERROR_STOP=1 --pset pager=off -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "$sql" -} - -echo "📋 数据库容器: $POSTGRES_CONTAINER" -echo "📋 连接参数: $POSTGRES_HOST:${POSTGRES_PORT}/$POSTGRES_DB (user: $POSTGRES_USER)" - -echo "📊 数据库概览:" -run_psql "SELECT relname AS \"表名\", n_live_tup AS \"记录数\" FROM pg_stat_user_tables WHERE n_live_tup > 0 ORDER BY relname;" - -echo -e "\n🤖 AI模型配置:" -run_psql "SELECT id, name, provider, enabled, CASE WHEN api_key != '' THEN '已配置' ELSE '未配置' END AS api_key_status FROM ai_models ORDER BY id;" - -echo -e "\n🏢 交易所配置:" -run_psql "SELECT id, name, type, enabled, CASE WHEN api_key != '' THEN '已配置' ELSE '未配置' END AS api_key_status FROM exchanges ORDER BY id;" - -echo -e "\n⚙️ 关键系统配置:" -run_psql "SELECT key, CASE WHEN LENGTH(value) > 50 THEN LEFT(value, 50) || '...' ELSE value END AS value FROM system_config WHERE key IN ('beta_mode', 'api_server_port', 'default_coins', 'jwt_secret') ORDER BY key;" - -echo -e "\n🎟️ 内测码统计:" -run_psql "SELECT CASE WHEN used THEN '已使用' ELSE '未使用' END AS status, COUNT(*) AS count FROM beta_codes GROUP BY used ORDER BY used;" - -echo -e "\n👥 用户信息:" -run_psql "SELECT id, email, otp_verified, created_at FROM users ORDER BY created_at;" diff --git a/scripts/start.sh b/start.sh similarity index 94% rename from scripts/start.sh rename to start.sh index 1a67d9e1..b9a84bac 100755 --- a/scripts/start.sh +++ b/start.sh @@ -173,28 +173,6 @@ check_config() { print_success "配置文件存在" } -# ------------------------------------------------------------------------ -# Validation: Beta Code File (beta_codes.txt) -# ------------------------------------------------------------------------ -check_beta_codes_file() { - local beta_file="beta_codes.txt" - - if [ -d "$beta_file" ]; then - print_warning "beta_codes.txt 是目录,正在删除后重建文件..." - rm -rf "$beta_file" - touch "$beta_file" - chmod 600 "$beta_file" - print_info "✓ 已重新创建 beta_codes.txt(权限: 600)" - elif [ ! -f "$beta_file" ]; then - print_warning "beta_codes.txt 不存在,正在创建空文件..." - touch "$beta_file" - chmod 600 "$beta_file" - print_info "✓ 已创建空的内测码文件(权限: 600)" - else - print_success "内测码文件存在" - fi -} - # ------------------------------------------------------------------------ # Utility: Read Environment Variables # ------------------------------------------------------------------------ @@ -282,7 +260,6 @@ start() { # 确保必要的文件和目录存在(修复 Docker volume 挂载问题) if [ ! -f "config.db" ]; then print_info "创建数据库文件..." - touch config.db install -m 600 /dev/null config.db fi if [ ! -d "decision_logs" ]; then @@ -436,7 +413,6 @@ main() { check_env check_encryption check_config - check_beta_codes_file check_database start "$2" ;; @@ -473,4 +449,4 @@ main() { } # Execute Main -main "$@" +main "$@" \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json index b1545cf4..85f06c30 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -120,7 +120,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -452,7 +451,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -476,7 +474,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -1170,6 +1167,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=18" } @@ -1181,6 +1179,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@inquirer/core": "^10.3.0", "@inquirer/type": "^3.0.9" @@ -1204,6 +1203,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@inquirer/ansi": "^1.0.1", "@inquirer/figures": "^1.0.14", @@ -1233,6 +1233,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -1244,6 +1245,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -1260,7 +1262,8 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/@inquirer/core/node_modules/string-width": { "version": "4.2.3", @@ -1269,6 +1272,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -1285,6 +1289,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -1299,6 +1304,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -1315,6 +1321,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=18" } @@ -1326,6 +1333,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=18" }, @@ -1407,6 +1415,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", @@ -1460,7 +1469,8 @@ "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/@open-draft/logger": { "version": "0.3.0", @@ -1469,6 +1479,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "is-node-process": "^1.2.0", "outvariant": "^1.4.0" @@ -1480,7 +1491,8 @@ "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", @@ -2258,7 +2270,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -2379,7 +2392,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "devOptional": true, - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -2390,7 +2402,6 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -2401,7 +2412,8 @@ "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.46.3", @@ -2439,7 +2451,6 @@ "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.3", "@typescript-eslint/types": "8.46.3", @@ -2764,7 +2775,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3188,7 +3198,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -3467,6 +3476,7 @@ "dev": true, "license": "ISC", "optional": true, + "peer": true, "engines": { "node": ">= 12" } @@ -3478,6 +3488,7 @@ "dev": true, "license": "ISC", "optional": true, + "peer": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -3494,6 +3505,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -3505,6 +3517,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -3521,7 +3534,8 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/cliui/node_modules/string-width": { "version": "4.2.3", @@ -3530,6 +3544,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -3546,6 +3561,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -3560,6 +3576,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -4031,7 +4048,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dom-helpers": { "version": "5.2.1", @@ -4354,7 +4372,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4415,7 +4432,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -5046,6 +5062,7 @@ "dev": true, "license": "ISC", "optional": true, + "peer": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -5218,6 +5235,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -5321,7 +5339,8 @@ "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/hermes-estree": { "version": "0.25.1", @@ -5724,7 +5743,8 @@ "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/is-number": { "version": "7.0.0", @@ -5955,7 +5975,6 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -5984,7 +6003,6 @@ "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.1.0", "data-urls": "^5.0.0", @@ -6359,6 +6377,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -6504,6 +6523,7 @@ "hasInstallScript": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.40.0", @@ -6549,6 +6569,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tldts-core": "^7.0.17" }, @@ -6562,7 +6583,8 @@ "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/msw/node_modules/tough-cookie": { "version": "6.0.0", @@ -6571,6 +6593,7 @@ "dev": true, "license": "BSD-3-Clause", "optional": true, + "peer": true, "dependencies": { "tldts": "^7.0.5" }, @@ -6585,6 +6608,7 @@ "dev": true, "license": "ISC", "optional": true, + "peer": true, "engines": { "node": "^18.17.0 || >=20.5.0" } @@ -6824,7 +6848,8 @@ "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/own-keys": { "version": "1.0.1", @@ -6961,7 +6986,8 @@ "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/pathe": { "version": "1.1.2", @@ -7058,7 +7084,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -7212,7 +7237,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -7242,6 +7266,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -7257,6 +7282,7 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -7267,6 +7293,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -7279,7 +7306,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/prop-types": { "version": "15.8.1", @@ -7330,7 +7358,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -7342,7 +7369,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -7626,6 +7652,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -7683,7 +7710,8 @@ "integrity": "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/reusify": { "version": "1.1.0", @@ -8102,6 +8130,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 0.8" } @@ -8133,7 +8162,8 @@ "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/string-argv": { "version": "0.3.2", @@ -8572,7 +8602,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, - "peer": true, "engines": { "node": ">=12" }, @@ -8713,6 +8742,7 @@ "dev": true, "license": "(MIT OR CC0-1.0)", "optional": true, + "peer": true, "engines": { "node": ">=16" }, @@ -8803,7 +8833,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8838,6 +8867,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "funding": { "url": "https://github.com/sponsors/kettanaito" } @@ -8965,7 +8995,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -9570,7 +9599,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, - "peer": true, "engines": { "node": ">=12" }, @@ -10107,7 +10135,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -10490,6 +10517,7 @@ "dev": true, "license": "ISC", "optional": true, + "peer": true, "engines": { "node": ">=10" } @@ -10520,6 +10548,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -10540,6 +10569,7 @@ "dev": true, "license": "ISC", "optional": true, + "peer": true, "engines": { "node": ">=12" } @@ -10551,6 +10581,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -10561,7 +10592,8 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/yargs/node_modules/string-width": { "version": "4.2.3", @@ -10570,6 +10602,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -10586,6 +10619,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -10613,6 +10647,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=18" }, @@ -10626,7 +10661,6 @@ "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/web/src/components/ComparisonChart.tsx b/web/src/components/ComparisonChart.tsx index 9789517d..dc81c9cf 100644 --- a/web/src/components/ComparisonChart.tsx +++ b/web/src/components/ComparisonChart.tsx @@ -269,20 +269,14 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
NOFX diff --git a/web/src/components/EquityChart.tsx b/web/src/components/EquityChart.tsx index e433ade0..f8beb1d5 100644 --- a/web/src/components/EquityChart.tsx +++ b/web/src/components/EquityChart.tsx @@ -301,20 +301,14 @@ export function EquityChart({ traderId }: EquityChartProps) {
NOFX diff --git a/web/src/components/landing/AboutSection.tsx b/web/src/components/landing/AboutSection.tsx index 97b0c21f..9ec4df0a 100644 --- a/web/src/components/landing/AboutSection.tsx +++ b/web/src/components/landing/AboutSection.tsx @@ -104,8 +104,8 @@ export default function AboutSection({ language }: AboutSectionProps) { lines={[ '$ git clone https://github.com/tinkle-community/nofx.git', '$ cd nofx', - '$ chmod +x scripts/start.sh', - '$ ./scripts/start.sh start --build', + '$ chmod +x start.sh', + '$ ./start.sh start --build', t('startupMessages1', language), t('startupMessages2', language), t('startupMessages3', language), diff --git a/web/src/components/landing/HeaderBar.tsx b/web/src/components/landing/HeaderBar.tsx deleted file mode 100644 index 665b0fd3..00000000 --- a/web/src/components/landing/HeaderBar.tsx +++ /dev/null @@ -1,815 +0,0 @@ -import { useState, useEffect, useRef } from 'react' -import { motion } from 'framer-motion' -import { Menu, X, ChevronDown } from 'lucide-react' -import { t, type Language } from '../../i18n/translations' - -interface HeaderBarProps { - onLoginClick?: () => void - isLoggedIn?: boolean - isHomePage?: boolean - currentPage?: string - language?: Language - onLanguageChange?: (lang: Language) => void - user?: { email: string } | null - onLogout?: () => void - onPageChange?: (page: string) => void -} - -export default function HeaderBar({ - isLoggedIn = false, - isHomePage = false, - currentPage, - language = 'zh' as Language, - onLanguageChange, - user, - onLogout, - onPageChange, -}: HeaderBarProps) { - const [mobileMenuOpen, setMobileMenuOpen] = useState(false) - const [languageDropdownOpen, setLanguageDropdownOpen] = useState(false) - const [userDropdownOpen, setUserDropdownOpen] = useState(false) - const dropdownRef = useRef(null) - const userDropdownRef = useRef(null) - - // Close dropdown when clicking outside - useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if ( - dropdownRef.current && - !dropdownRef.current.contains(event.target as Node) - ) { - setLanguageDropdownOpen(false) - } - if ( - userDropdownRef.current && - !userDropdownRef.current.contains(event.target as Node) - ) { - setUserDropdownOpen(false) - } - } - - document.addEventListener('mousedown', handleClickOutside) - return () => { - document.removeEventListener('mousedown', handleClickOutside) - } - }, []) - - return ( - - ) -} diff --git a/web/src/lib/config.ts b/web/src/lib/config.ts index 0f137d2c..6be8f034 100644 --- a/web/src/lib/config.ts +++ b/web/src/lib/config.ts @@ -1,10 +1,5 @@ export interface SystemConfig { beta_mode: boolean - default_coins?: string[] - btc_eth_leverage?: number - altcoin_leverage?: number - rsa_public_key?: string - rsa_key_id?: string } let configPromise: Promise | null = null diff --git a/web/src/types.ts b/web/src/types.ts index bf88bb96..60ce44ed 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -108,16 +108,18 @@ export interface AIModel { export interface Exchange { id: string - user_id: string name: string type: 'cex' | 'dex' enabled: boolean + apiKey?: string + secretKey?: string testnet?: boolean - hyperliquidWalletAddr?: string // 钱包地址,非敏感信息 - asterUser?: string // Aster用户名,非敏感信息 - deleted: boolean - created_at: string - updated_at: string + // Hyperliquid 特定字段 + hyperliquidWalletAddr?: string + // Aster 特定字段 + asterUser?: string + asterSigner?: string + asterPrivateKey?: string } export interface CreateTraderRequest {