diff --git a/api/server.go b/api/server.go index ad16470a..94ae4a60 100644 --- a/api/server.go +++ b/api/server.go @@ -71,29 +71,37 @@ func (s *Server) setupRoutes() { { // 健康检查 api.Any("/health", s.handleHealth) - + // 认证相关路由(无需认证) api.POST("/register", s.handleRegister) api.POST("/login", s.handleLogin) api.POST("/verify-otp", s.handleVerifyOTP) api.POST("/complete-registration", s.handleCompleteRegistration) - + // 系统支持的模型和交易所(无需认证) api.GET("/supported-models", s.handleGetSupportedModels) api.GET("/supported-exchanges", s.handleGetSupportedExchanges) - + // 系统配置(无需认证) api.GET("/config", s.handleGetSystemConfig) - + // 系统提示词模板管理(无需认证) api.GET("/prompt-templates", s.handleGetPromptTemplates) api.GET("/prompt-templates/:name", s.handleGetPromptTemplate) + + // 公开的竞赛数据(无需认证) + api.GET("/traders", s.handlePublicTraderList) + api.GET("/competition", s.handlePublicCompetition) + api.GET("/top-traders", s.handleTopTraders) + api.GET("/equity-history", s.handleEquityHistory) + api.POST("/equity-history-batch", s.handleEquityHistoryBatch) + api.GET("/traders/:id/public-config", s.handleGetPublicTraderConfig) // 需要认证的路由 protected := api.Group("/", s.authMiddleware()) { // AI交易员管理 - protected.GET("/traders", s.handleTraderList) + protected.GET("/my-traders", s.handleTraderList) protected.GET("/traders/:id/config", s.handleGetTraderConfig) protected.POST("/traders", s.handleCreateTrader) protected.PUT("/traders/:id", s.handleUpdateTrader) @@ -114,10 +122,6 @@ func (s *Server) setupRoutes() { protected.GET("/user/signal-sources", s.handleGetUserSignalSource) protected.POST("/user/signal-sources", s.handleSaveUserSignalSource) - - // 竞赛总览 - protected.GET("/competition", s.handleCompetition) - // 指定trader的数据(使用query参数 ?trader_id=xxx) protected.GET("/status", s.handleStatus) protected.GET("/account", s.handleAccount) @@ -125,7 +129,6 @@ func (s *Server) setupRoutes() { protected.GET("/decisions", s.handleDecisions) protected.GET("/decisions/latest", s.handleLatestDecisions) protected.GET("/statistics", s.handleStatistics) - protected.GET("/equity-history", s.handleEquityHistory) protected.GET("/performance", s.handlePerformance) } } @@ -151,24 +154,29 @@ func (s *Server) handleGetSystemConfig(c *gin.Context) { // 使用硬编码的默认币种 defaultCoins = []string{"BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "HYPEUSDT"} } - + // 获取杠杆配置 btcEthLeverageStr, _ := s.database.GetSystemConfig("btc_eth_leverage") altcoinLeverageStr, _ := s.database.GetSystemConfig("altcoin_leverage") - + btcEthLeverage := 5 if val, err := strconv.Atoi(btcEthLeverageStr); err == nil && val > 0 { btcEthLeverage = val } - + altcoinLeverage := 5 if val, err := strconv.Atoi(altcoinLeverageStr); err == nil && val > 0 { altcoinLeverage = val } + // 获取内测模式配置 + betaModeStr, _ := s.database.GetSystemConfig("beta_mode") + betaMode := betaModeStr == "true" + c.JSON(http.StatusOK, gin.H{ - "admin_mode": auth.IsAdminMode(), - "default_coins": defaultCoins, + "admin_mode": auth.IsAdminMode(), + "beta_mode": betaMode, + "default_coins": defaultCoins, "btc_eth_leverage": btcEthLeverage, "altcoin_leverage": altcoinLeverage, }) @@ -178,20 +186,20 @@ func (s *Server) handleGetSystemConfig(c *gin.Context) { func (s *Server) getTraderFromQuery(c *gin.Context) (*manager.TraderManager, string, error) { userID := c.GetString("user_id") traderID := c.Query("trader_id") - + // 确保用户的交易员已加载到内存中 err := s.traderManager.LoadUserTraders(s.database, userID) if err != nil { log.Printf("⚠️ 加载用户 %s 的交易员失败: %v", userID, err) } - + if traderID == "" { // 如果没有指定trader_id,返回该用户的第一个trader ids := s.traderManager.GetTraderIDs() if len(ids) == 0 { return nil, "", fmt.Errorf("没有可用的trader") } - + // 获取用户的交易员列表,优先返回用户自己的交易员 userTraders, err := s.database.GetTraders(userID) if err == nil && len(userTraders) > 0 { @@ -200,7 +208,7 @@ func (s *Server) getTraderFromQuery(c *gin.Context) (*manager.TraderManager, str traderID = ids[0] } } - + return s.traderManager, traderID, nil } @@ -210,6 +218,7 @@ type CreateTraderRequest struct { AIModelID string `json:"ai_model_id" binding:"required"` ExchangeID string `json:"exchange_id" binding:"required"` InitialBalance float64 `json:"initial_balance"` + ScanIntervalMinutes int `json:"scan_interval_minutes"` BTCETHLeverage int `json:"btc_eth_leverage"` AltcoinLeverage int `json:"altcoin_leverage"` TradingSymbols string `json:"trading_symbols"` @@ -295,13 +304,13 @@ func (s *Server) handleCreateTrader(c *gin.Context) { // 生成交易员ID traderID := fmt.Sprintf("%s_%s_%d", req.ExchangeID, req.AIModelID, time.Now().Unix()) - + // 设置默认值 isCrossMargin := true // 默认为全仓模式 if req.IsCrossMargin != nil { isCrossMargin = *req.IsCrossMargin } - + // 设置杠杆默认值(从系统配置获取) btcEthLeverage := 5 altcoinLeverage := 5 @@ -325,15 +334,21 @@ func (s *Server) handleCreateTrader(c *gin.Context) { } } } - + // 设置系统提示词模板默认值 systemPromptTemplate := "default" if req.SystemPromptTemplate != "" { systemPromptTemplate = req.SystemPromptTemplate } - // 创建交易员配置(数据库实体) - trader := &config.TraderRecord{ + // 设置扫描间隔默认值 + scanIntervalMinutes := req.ScanIntervalMinutes + if scanIntervalMinutes <= 0 { + scanIntervalMinutes = 3 // 默认3分钟 + } + + // 创建交易员配置(数据库实体) + trader := &config.TraderRecord{ ID: traderID, UserID: userID, Name: req.Name, @@ -349,8 +364,8 @@ func (s *Server) handleCreateTrader(c *gin.Context) { OverrideBasePrompt: req.OverrideBasePrompt, SystemPromptTemplate: systemPromptTemplate, IsCrossMargin: isCrossMargin, - ScanIntervalMinutes: 3, // 默认3分钟 - IsRunning: false, + ScanIntervalMinutes: scanIntervalMinutes, + IsRunning: false, } // 保存到数据库 @@ -379,23 +394,24 @@ func (s *Server) handleCreateTrader(c *gin.Context) { // UpdateTraderRequest 更新交易员请求 type UpdateTraderRequest struct { - Name string `json:"name" binding:"required"` - AIModelID string `json:"ai_model_id" binding:"required"` - ExchangeID string `json:"exchange_id" binding:"required"` - InitialBalance float64 `json:"initial_balance"` - BTCETHLeverage int `json:"btc_eth_leverage"` - AltcoinLeverage int `json:"altcoin_leverage"` - TradingSymbols string `json:"trading_symbols"` - CustomPrompt string `json:"custom_prompt"` - OverrideBasePrompt bool `json:"override_base_prompt"` - IsCrossMargin *bool `json:"is_cross_margin"` + Name string `json:"name" binding:"required"` + AIModelID string `json:"ai_model_id" binding:"required"` + ExchangeID string `json:"exchange_id" binding:"required"` + InitialBalance float64 `json:"initial_balance"` + ScanIntervalMinutes int `json:"scan_interval_minutes"` + BTCETHLeverage int `json:"btc_eth_leverage"` + AltcoinLeverage int `json:"altcoin_leverage"` + TradingSymbols string `json:"trading_symbols"` + CustomPrompt string `json:"custom_prompt"` + OverrideBasePrompt bool `json:"override_base_prompt"` + IsCrossMargin *bool `json:"is_cross_margin"` } // handleUpdateTrader 更新交易员配置 func (s *Server) handleUpdateTrader(c *gin.Context) { userID := c.GetString("user_id") traderID := c.Param("id") - + var req UpdateTraderRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) @@ -408,7 +424,7 @@ func (s *Server) handleUpdateTrader(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "获取交易员列表失败"}) return } - + var existingTrader *config.TraderRecord for _, trader := range traders { if trader.ID == traderID { @@ -416,7 +432,7 @@ func (s *Server) handleUpdateTrader(c *gin.Context) { break } } - + if existingTrader == nil { c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) return @@ -427,7 +443,7 @@ func (s *Server) handleUpdateTrader(c *gin.Context) { if req.IsCrossMargin != nil { isCrossMargin = *req.IsCrossMargin } - + // 设置杠杆默认值 btcEthLeverage := req.BTCETHLeverage altcoinLeverage := req.AltcoinLeverage @@ -437,23 +453,30 @@ func (s *Server) handleUpdateTrader(c *gin.Context) { if altcoinLeverage <= 0 { altcoinLeverage = existingTrader.AltcoinLeverage // 保持原值 } - - // 更新交易员配置 - trader := &config.TraderRecord{ - ID: traderID, - UserID: userID, - Name: req.Name, - AIModelID: req.AIModelID, - ExchangeID: req.ExchangeID, - InitialBalance: req.InitialBalance, - BTCETHLeverage: btcEthLeverage, - AltcoinLeverage: altcoinLeverage, - TradingSymbols: req.TradingSymbols, - CustomPrompt: req.CustomPrompt, - OverrideBasePrompt: req.OverrideBasePrompt, - IsCrossMargin: isCrossMargin, - ScanIntervalMinutes: existingTrader.ScanIntervalMinutes, // 保持原值 - IsRunning: existingTrader.IsRunning, // 保持原值 + + // 设置扫描间隔,允许更新 + scanIntervalMinutes := req.ScanIntervalMinutes + if scanIntervalMinutes <= 0 { + scanIntervalMinutes = existingTrader.ScanIntervalMinutes // 保持原值 + } + + // 更新交易员配置 + trader := &config.TraderRecord{ + ID: traderID, + UserID: userID, + Name: req.Name, + AIModelID: req.AIModelID, + ExchangeID: req.ExchangeID, + InitialBalance: req.InitialBalance, + BTCETHLeverage: btcEthLeverage, + AltcoinLeverage: altcoinLeverage, + TradingSymbols: req.TradingSymbols, + CustomPrompt: req.CustomPrompt, + OverrideBasePrompt: req.OverrideBasePrompt, + SystemPromptTemplate: existingTrader.SystemPromptTemplate, // 保持原值 + IsCrossMargin: isCrossMargin, + ScanIntervalMinutes: scanIntervalMinutes, + IsRunning: existingTrader.IsRunning, // 保持原值 } // 更新数据库 @@ -483,14 +506,14 @@ func (s *Server) handleUpdateTrader(c *gin.Context) { func (s *Server) handleDeleteTrader(c *gin.Context) { userID := c.GetString("user_id") traderID := c.Param("id") - + // 从数据库删除 err := s.database.DeleteTrader(userID, traderID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("删除交易员失败: %v", err)}) return } - + // 如果交易员正在运行,先停止它 if trader, err := s.traderManager.GetTrader(traderID); err == nil { status := trader.GetStatus() @@ -499,28 +522,36 @@ func (s *Server) handleDeleteTrader(c *gin.Context) { log.Printf("⏹ 已停止运行中的交易员: %s", traderID) } } - + log.Printf("✓ 交易员已删除: %s", traderID) c.JSON(http.StatusOK, gin.H{"message": "交易员已删除"}) } // handleStartTrader 启动交易员 func (s *Server) handleStartTrader(c *gin.Context) { + userID := c.GetString("user_id") traderID := c.Param("id") + // 校验交易员是否属于当前用户 + _, _, _, err := s.database.GetTraderConfig(userID, traderID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在或无访问权限"}) + return + } + trader, err := s.traderManager.GetTrader(traderID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) return } - + // 检查交易员是否已经在运行 status := trader.GetStatus() if isRunning, ok := status["is_running"].(bool); ok && isRunning { c.JSON(http.StatusBadRequest, gin.H{"error": "交易员已在运行中"}) return } - + // 启动交易员 go func() { log.Printf("▶️ 启动交易员 %s (%s)", traderID, trader.GetName()) @@ -528,45 +559,51 @@ func (s *Server) handleStartTrader(c *gin.Context) { log.Printf("❌ 交易员 %s 运行错误: %v", trader.GetName(), err) } }() - + // 更新数据库中的运行状态 - userID := c.GetString("user_id") err = s.database.UpdateTraderStatus(userID, traderID, true) if err != nil { log.Printf("⚠️ 更新交易员状态失败: %v", err) } - + log.Printf("✓ 交易员 %s 已启动", trader.GetName()) c.JSON(http.StatusOK, gin.H{"message": "交易员已启动"}) } // handleStopTrader 停止交易员 func (s *Server) handleStopTrader(c *gin.Context) { + userID := c.GetString("user_id") traderID := c.Param("id") + // 校验交易员是否属于当前用户 + _, _, _, err := s.database.GetTraderConfig(userID, traderID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在或无访问权限"}) + return + } + trader, err := s.traderManager.GetTrader(traderID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) return } - + // 检查交易员是否正在运行 status := trader.GetStatus() if isRunning, ok := status["is_running"].(bool); ok && !isRunning { c.JSON(http.StatusBadRequest, gin.H{"error": "交易员已停止"}) return } - + // 停止交易员 trader.Stop() - + // 更新数据库中的运行状态 - userID := c.GetString("user_id") err = s.database.UpdateTraderStatus(userID, traderID, false) if err != nil { log.Printf("⚠️ 更新交易员状态失败: %v", err) } - + log.Printf("⏹ 交易员 %s 已停止", trader.GetName()) c.JSON(http.StatusOK, gin.H{"message": "交易员已停止"}) } @@ -575,24 +612,24 @@ func (s *Server) handleStopTrader(c *gin.Context) { func (s *Server) handleUpdateTraderPrompt(c *gin.Context) { traderID := c.Param("id") userID := c.GetString("user_id") - + var req struct { - CustomPrompt string `json:"custom_prompt"` - OverrideBasePrompt bool `json:"override_base_prompt"` + CustomPrompt string `json:"custom_prompt"` + OverrideBasePrompt bool `json:"override_base_prompt"` } - + if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - + // 更新数据库 err := s.database.UpdateTraderCustomPrompt(userID, traderID, req.CustomPrompt, req.OverrideBasePrompt) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新自定义prompt失败: %v", err)}) return } - + // 如果trader在内存中,更新其custom prompt和override设置 trader, err := s.traderManager.GetTrader(traderID) if err == nil { @@ -600,7 +637,7 @@ func (s *Server) handleUpdateTraderPrompt(c *gin.Context) { trader.SetOverrideBasePrompt(req.OverrideBasePrompt) log.Printf("✓ 已更新交易员 %s 的自定义prompt (覆盖基础=%v)", trader.GetName(), req.OverrideBasePrompt) } - + c.JSON(http.StatusOK, gin.H{"message": "自定义prompt已更新"}) } @@ -615,7 +652,7 @@ func (s *Server) handleGetModelConfigs(c *gin.Context) { return } log.Printf("✅ 找到 %d 个AI模型配置", len(models)) - + c.JSON(http.StatusOK, models) } @@ -659,7 +696,7 @@ func (s *Server) handleGetExchangeConfigs(c *gin.Context) { return } log.Printf("✅ 找到 %d 个交易所配置", len(exchanges)) - + c.JSON(http.StatusOK, exchanges) } @@ -704,7 +741,7 @@ func (s *Server) handleGetUserSignalSource(c *gin.Context) { }) return } - + c.JSON(http.StatusOK, gin.H{ "coin_pool_url": source.CoinPoolURL, "oi_top_url": source.OITopURL, @@ -718,18 +755,18 @@ func (s *Server) handleSaveUserSignalSource(c *gin.Context) { CoinPoolURL string `json:"coin_pool_url"` OITopURL string `json:"oi_top_url"` } - + if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - + err := s.database.CreateUserSignalSource(userID, req.CoinPoolURL, req.OITopURL) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("保存用户信号源配置失败: %v", err)}) return } - + log.Printf("✓ 用户信号源配置已保存: user=%s, coin_pool=%s, oi_top=%s", userID, req.CoinPoolURL, req.OITopURL) c.JSON(http.StatusOK, gin.H{"message": "用户信号源配置已保存"}) } @@ -801,30 +838,25 @@ func (s *Server) handleGetTraderConfig(c *gin.Context) { } } - // AIModelID 应该已经是 provider(如 "deepseek"),直接使用 - // 如果是旧数据格式(如 "admin_deepseek"),提取 provider 部分 + // 返回完整的模型ID,不做转换,保持与前端模型列表一致 aiModelID := traderConfig.AIModelID - // 兼容旧数据:如果包含下划线,提取最后一部分作为 provider - if strings.Contains(aiModelID, "_") { - parts := strings.Split(aiModelID, "_") - aiModelID = parts[len(parts)-1] - } result := map[string]interface{}{ - "trader_id": traderConfig.ID, - "trader_name": traderConfig.Name, - "ai_model": aiModelID, - "exchange_id": traderConfig.ExchangeID, - "initial_balance": traderConfig.InitialBalance, - "btc_eth_leverage": traderConfig.BTCETHLeverage, - "altcoin_leverage": traderConfig.AltcoinLeverage, - "trading_symbols": traderConfig.TradingSymbols, - "custom_prompt": traderConfig.CustomPrompt, - "override_base_prompt": traderConfig.OverrideBasePrompt, - "is_cross_margin": traderConfig.IsCrossMargin, - "use_coin_pool": traderConfig.UseCoinPool, - "use_oi_top": traderConfig.UseOITop, - "is_running": isRunning, + "trader_id": traderConfig.ID, + "trader_name": traderConfig.Name, + "ai_model": aiModelID, + "exchange_id": traderConfig.ExchangeID, + "initial_balance": traderConfig.InitialBalance, + "scan_interval_minutes": traderConfig.ScanIntervalMinutes, + "btc_eth_leverage": traderConfig.BTCETHLeverage, + "altcoin_leverage": traderConfig.AltcoinLeverage, + "trading_symbols": traderConfig.TradingSymbols, + "custom_prompt": traderConfig.CustomPrompt, + "override_base_prompt": traderConfig.OverrideBasePrompt, + "is_cross_margin": traderConfig.IsCrossMargin, + "use_coin_pool": traderConfig.UseCoinPool, + "use_oi_top": traderConfig.UseOITop, + "is_running": isRunning, } c.JSON(http.StatusOK, result) @@ -991,13 +1023,13 @@ func (s *Server) handleStatistics(c *gin.Context) { // handleCompetition 竞赛总览(对比所有trader) func (s *Server) handleCompetition(c *gin.Context) { userID := c.GetString("user_id") - + // 确保用户的交易员已加载到内存中 err := s.traderManager.LoadUserTraders(s.database, userID) if err != nil { log.Printf("⚠️ 加载用户 %s 的交易员失败: %v", userID, err) } - + competition, err := s.traderManager.GetCompetitionData() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ @@ -1005,7 +1037,7 @@ func (s *Server) handleCompetition(c *gin.Context) { }) return } - + c.JSON(http.StatusOK, competition) } @@ -1132,7 +1164,7 @@ func (s *Server) authMiddleware() gin.HandlerFunc { c.Next() return } - + authHeader := c.GetHeader("Authorization") if authHeader == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "缺少Authorization头"}) @@ -1168,6 +1200,7 @@ func (s *Server) handleRegister(c *gin.Context) { var req struct { Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=6"` + BetaCode string `json:"beta_code"` } if err := c.ShouldBindJSON(&req); err != nil { @@ -1175,6 +1208,27 @@ func (s *Server) handleRegister(c *gin.Context) { return } + // 检查是否开启了内测模式 + betaModeStr, _ := s.database.GetSystemConfig("beta_mode") + if betaModeStr == "true" { + // 内测模式下必须提供有效的内测码 + if req.BetaCode == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "内测期间,注册需要提供内测码"}) + return + } + + // 验证内测码 + isValid, err := s.database.ValidateBetaCode(req.BetaCode) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "验证内测码失败"}) + return + } + if !isValid { + c.JSON(http.StatusBadRequest, gin.H{"error": "内测码无效或已被使用"}) + return + } + } + // 检查邮箱是否已存在 _, err := s.database.GetUserByEmail(req.Email) if err == nil { @@ -1212,14 +1266,26 @@ func (s *Server) handleRegister(c *gin.Context) { return } + // 如果是内测模式,标记内测码为已使用 + betaModeStr2, _ := s.database.GetSystemConfig("beta_mode") + if betaModeStr2 == "true" && req.BetaCode != "" { + err := s.database.UseBetaCode(req.BetaCode, req.Email) + if err != nil { + log.Printf("⚠️ 标记内测码为已使用失败: %v", err) + // 这里不返回错误,因为用户已经创建成功 + } else { + log.Printf("✓ 内测码 %s 已被用户 %s 使用", req.BetaCode, req.Email) + } + } + // 返回OTP设置信息 qrCodeURL := auth.GetOTPQRCodeURL(otpSecret, req.Email) c.JSON(http.StatusOK, gin.H{ - "user_id": userID, - "email": req.Email, - "otp_secret": otpSecret, + "user_id": userID, + "email": req.Email, + "otp_secret": otpSecret, "qr_code_url": qrCodeURL, - "message": "请使用Google Authenticator扫描二维码并验证OTP", + "message": "请使用Google Authenticator扫描二维码并验证OTP", }) } @@ -1304,8 +1370,8 @@ func (s *Server) handleLogin(c *gin.Context) { // 检查OTP是否已验证 if !user.OTPVerified { c.JSON(http.StatusUnauthorized, gin.H{ - "error": "账户未完成OTP设置", - "user_id": user.ID, + "error": "账户未完成OTP设置", + "user_id": user.ID, "requires_otp_setup": true, }) return @@ -1313,9 +1379,9 @@ func (s *Server) handleLogin(c *gin.Context) { // 返回需要OTP验证的状态 c.JSON(http.StatusOK, gin.H{ - "user_id": user.ID, - "email": user.Email, - "message": "请输入Google Authenticator验证码", + "user_id": user.ID, + "email": user.Email, + "message": "请输入Google Authenticator验证码", "requires_otp": true, }) } @@ -1377,7 +1443,7 @@ func (s *Server) handleGetSupportedModels(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "获取支持的AI模型失败"}) return } - + c.JSON(http.StatusOK, models) } @@ -1390,7 +1456,7 @@ func (s *Server) handleGetSupportedExchanges(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "获取支持的交易所失败"}) return } - + c.JSON(http.StatusOK, exchanges) } @@ -1400,7 +1466,12 @@ func (s *Server) Start() error { log.Printf("🌐 API服务器启动在 http://localhost%s", addr) log.Printf("📊 API文档:") log.Printf(" • GET /api/health - 健康检查") - log.Printf(" • GET /api/traders - AI交易员列表") + log.Printf(" • GET /api/traders - 公开的AI交易员排行榜前50名(无需认证)") + log.Printf(" • GET /api/competition - 公开的竞赛数据(无需认证)") + log.Printf(" • GET /api/top-traders - 前5名交易员数据(无需认证,表现对比用)") + log.Printf(" • GET /api/equity-history?trader_id=xxx - 公开的收益率历史数据(无需认证,竞赛用)") + log.Printf(" • GET /api/equity-history-batch?trader_ids=a,b,c - 批量获取历史数据(无需认证,表现对比优化)") + log.Printf(" • GET /api/traders/:id/public-config - 公开的交易员配置(无需认证,不含敏感信息)") log.Printf(" • POST /api/traders - 创建新的AI交易员") log.Printf(" • DELETE /api/traders/:id - 删除AI交易员") log.Printf(" • POST /api/traders/:id/start - 启动AI交易员") @@ -1415,7 +1486,6 @@ func (s *Server) Start() error { log.Printf(" • GET /api/decisions?trader_id=xxx - 指定trader的决策日志") log.Printf(" • GET /api/decisions/latest?trader_id=xxx - 指定trader的最新决策") log.Printf(" • GET /api/statistics?trader_id=xxx - 指定trader的统计信息") - log.Printf(" • GET /api/equity-history?trader_id=xxx - 指定trader的收益率历史数据") log.Printf(" • GET /api/performance?trader_id=xxx - 指定trader的AI学习表现分析") log.Println() @@ -1426,7 +1496,7 @@ func (s *Server) Start() error { func (s *Server) handleGetPromptTemplates(c *gin.Context) { // 导入 decision 包 templates := decision.GetAllPromptTemplates() - + // 转换为响应格式 response := make([]map[string]interface{}, 0, len(templates)) for _, tmpl := range templates { @@ -1434,7 +1504,7 @@ func (s *Server) handleGetPromptTemplates(c *gin.Context) { "name": tmpl.Name, }) } - + c.JSON(http.StatusOK, gin.H{ "templates": response, }) @@ -1443,15 +1513,224 @@ func (s *Server) handleGetPromptTemplates(c *gin.Context) { // handleGetPromptTemplate 获取指定名称的提示词模板内容 func (s *Server) handleGetPromptTemplate(c *gin.Context) { templateName := c.Param("name") - + template, err := decision.GetPromptTemplate(templateName) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("模板不存在: %s", templateName)}) return } - + c.JSON(http.StatusOK, gin.H{ "name": template.Name, "content": template.Content, }) } + +// handlePublicTraderList 获取公开的交易员列表(无需认证) +func (s *Server) handlePublicTraderList(c *gin.Context) { + // 从所有用户获取交易员信息 + competition, err := s.traderManager.GetCompetitionData() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": fmt.Sprintf("获取交易员列表失败: %v", err), + }) + return + } + + // 获取traders数组 + tradersData, exists := competition["traders"] + if !exists { + c.JSON(http.StatusOK, []map[string]interface{}{}) + return + } + + traders, ok := tradersData.([]map[string]interface{}) + if !ok { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "交易员数据格式错误", + }) + return + } + + // 返回交易员基本信息,过滤敏感信息 + result := make([]map[string]interface{}, 0, len(traders)) + for _, trader := range traders { + result = append(result, map[string]interface{}{ + "trader_id": trader["trader_id"], + "trader_name": trader["trader_name"], + "ai_model": trader["ai_model"], + "exchange": trader["exchange"], + "is_running": trader["is_running"], + "total_equity": trader["total_equity"], + "total_pnl": trader["total_pnl"], + "total_pnl_pct": trader["total_pnl_pct"], + "position_count": trader["position_count"], + "margin_used_pct": trader["margin_used_pct"], + }) + } + + c.JSON(http.StatusOK, result) +} + +// handlePublicCompetition 获取公开的竞赛数据(无需认证) +func (s *Server) handlePublicCompetition(c *gin.Context) { + competition, err := s.traderManager.GetCompetitionData() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": fmt.Sprintf("获取竞赛数据失败: %v", err), + }) + return + } + + c.JSON(http.StatusOK, competition) +} + +// handleTopTraders 获取前5名交易员数据(无需认证,用于表现对比) +func (s *Server) handleTopTraders(c *gin.Context) { + topTraders, err := s.traderManager.GetTopTradersData() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": fmt.Sprintf("获取前10名交易员数据失败: %v", err), + }) + return + } + + c.JSON(http.StatusOK, topTraders) +} + +// handleEquityHistoryBatch 批量获取多个交易员的收益率历史数据(无需认证,用于表现对比) +func (s *Server) handleEquityHistoryBatch(c *gin.Context) { + var requestBody struct { + TraderIDs []string `json:"trader_ids"` + } + + // 尝试解析POST请求的JSON body + if err := c.ShouldBindJSON(&requestBody); err != nil { + // 如果JSON解析失败,尝试从query参数获取(兼容GET请求) + traderIDsParam := c.Query("trader_ids") + if traderIDsParam == "" { + // 如果没有指定trader_ids,则返回前5名的历史数据 + topTraders, err := s.traderManager.GetTopTradersData() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": fmt.Sprintf("获取前5名交易员失败: %v", err), + }) + return + } + + traders, ok := topTraders["traders"].([]map[string]interface{}) + if !ok { + c.JSON(http.StatusInternalServerError, gin.H{"error": "交易员数据格式错误"}) + return + } + + // 提取trader IDs + traderIDs := make([]string, 0, len(traders)) + for _, trader := range traders { + if traderID, ok := trader["trader_id"].(string); ok { + traderIDs = append(traderIDs, traderID) + } + } + + result := s.getEquityHistoryForTraders(traderIDs) + c.JSON(http.StatusOK, result) + return + } + + // 解析逗号分隔的trader IDs + requestBody.TraderIDs = strings.Split(traderIDsParam, ",") + for i := range requestBody.TraderIDs { + requestBody.TraderIDs[i] = strings.TrimSpace(requestBody.TraderIDs[i]) + } + } + + // 限制最多20个交易员,防止请求过大 + if len(requestBody.TraderIDs) > 20 { + requestBody.TraderIDs = requestBody.TraderIDs[:20] + } + + result := s.getEquityHistoryForTraders(requestBody.TraderIDs) + c.JSON(http.StatusOK, result) +} + +// getEquityHistoryForTraders 获取多个交易员的历史数据 +func (s *Server) getEquityHistoryForTraders(traderIDs []string) map[string]interface{} { + result := make(map[string]interface{}) + histories := make(map[string]interface{}) + errors := make(map[string]string) + + for _, traderID := range traderIDs { + if traderID == "" { + continue + } + + trader, err := s.traderManager.GetTrader(traderID) + if err != nil { + errors[traderID] = "交易员不存在" + continue + } + + // 获取历史数据(用于对比展示,限制数据量) + records, err := trader.GetDecisionLogger().GetLatestRecords(500) + if err != nil { + errors[traderID] = fmt.Sprintf("获取历史数据失败: %v", err) + continue + } + + // 构建收益率历史数据 + history := make([]map[string]interface{}, 0, len(records)) + for _, record := range records { + // 计算总权益(余额+未实现盈亏) + totalEquity := record.AccountState.TotalBalance + record.AccountState.TotalUnrealizedProfit + + history = append(history, map[string]interface{}{ + "timestamp": record.Timestamp, + "total_equity": totalEquity, + "total_pnl": record.AccountState.TotalUnrealizedProfit, + "balance": record.AccountState.TotalBalance, + }) + } + + histories[traderID] = history + } + + result["histories"] = histories + result["count"] = len(histories) + if len(errors) > 0 { + result["errors"] = errors + } + + return result +} + +// handleGetPublicTraderConfig 获取公开的交易员配置信息(无需认证,不包含敏感信息) +func (s *Server) handleGetPublicTraderConfig(c *gin.Context) { + traderID := c.Param("id") + if traderID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "交易员ID不能为空"}) + return + } + + trader, err := s.traderManager.GetTrader(traderID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) + return + } + + // 获取交易员的状态信息 + status := trader.GetStatus() + + // 只返回公开的配置信息,不包含API密钥等敏感数据 + result := map[string]interface{}{ + "trader_id": trader.GetID(), + "trader_name": trader.GetName(), + "ai_model": trader.GetAIModel(), + "exchange": trader.GetExchange(), + "is_running": status["is_running"], + "ai_provider": status["ai_provider"], + "start_time": status["start_time"], + } + + c.JSON(http.StatusOK, result) +} + diff --git a/auth/auth.go b/auth/auth.go index 685d08e6..89c58e5c 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -61,7 +61,7 @@ func GenerateOTPSecret() (string, error) { if err != nil { return "", err } - + key, err := totp.Generate(totp.GenerateOpts{ Issuer: OTPIssuer, AccountName: uuid.New().String(), @@ -69,7 +69,7 @@ func GenerateOTPSecret() (string, error) { if err != nil { return "", err } - + return key.Secret(), nil } @@ -118,4 +118,4 @@ func ValidateJWT(tokenString string) (*Claims, error) { // GetOTPQRCodeURL 获取OTP二维码URL func GetOTPQRCodeURL(secret, email string) string { return fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s", OTPIssuer, email, secret, OTPIssuer) -} \ No newline at end of file +} diff --git a/config.json.example b/config.json.example index ac9d5ac6..fefa1673 100644 --- a/config.json.example +++ b/config.json.example @@ -1,5 +1,6 @@ { "admin_mode": true, + "beta_mode": false, "leverage": { "btc_eth_leverage": 5, "altcoin_leverage": 5 diff --git a/config/database.go b/config/database.go index 1102c6fb..719fd07f 100644 --- a/config/database.go +++ b/config/database.go @@ -8,6 +8,7 @@ import ( "fmt" "log" "nofx/market" + "os" "slices" "strings" "time" @@ -128,6 +129,15 @@ func (d *Database) createTables() error { updated_at DATETIME DEFAULT CURRENT_TIMESTAMP )`, + // 内测码表 + `CREATE TABLE IF NOT EXISTS beta_codes ( + code TEXT PRIMARY KEY, + used BOOLEAN DEFAULT 0, + used_by TEXT DEFAULT '', + used_at DATETIME DEFAULT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`, + // 触发器:自动更新 updated_at `CREATE TRIGGER IF NOT EXISTS update_users_updated_at AFTER UPDATE ON users @@ -188,7 +198,6 @@ func (d *Database) createTables() error { `ALTER TABLE traders ADD COLUMN trading_symbols TEXT DEFAULT ''`, // 交易币种,逗号分隔 `ALTER TABLE traders ADD COLUMN use_coin_pool BOOLEAN DEFAULT 0`, // 是否使用COIN POOL信号源 `ALTER TABLE traders ADD COLUMN use_oi_top BOOLEAN DEFAULT 0`, // 是否使用OI TOP信号源 - `ALTER TABLE traders ADD COLUMN use_inside_coins BOOLEAN DEFAULT 0`, // 是否使用内置AI评分信号源 `ALTER TABLE traders ADD COLUMN system_prompt_template TEXT DEFAULT 'default'`, // 系统提示词模板名称 `ALTER TABLE ai_models ADD COLUMN custom_api_url TEXT DEFAULT ''`, // 自定义API地址 `ALTER TABLE ai_models ADD COLUMN custom_model_name TEXT DEFAULT ''`, // 自定义模型名称 @@ -249,16 +258,17 @@ func (d *Database) initDefaultData() error { // 初始化系统配置 - 创建所有字段,设置默认值,后续由config.json同步更新 systemConfigs := map[string]string{ - "admin_mode": "true", // 默认开启管理员模式,便于首次使用 - "api_server_port": "8080", // 默认API端口 - "use_default_coins": "true", // 默认使用内置币种列表 - "default_coins": `["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]`, // 默认币种列表(JSON格式) - "max_daily_loss": "10.0", // 最大日损失百分比 - "max_drawdown": "20.0", // 最大回撤百分比 - "stop_trading_minutes": "60", // 停止交易时间(分钟) - "btc_eth_leverage": "5", // BTC/ETH杠杆倍数 - "altcoin_leverage": "5", // 山寨币杠杆倍数 - "jwt_secret": "", // JWT密钥,默认为空,由config.json或系统生成 + "admin_mode": "true", // 默认开启管理员模式,便于首次使用 + "beta_mode": "false", // 默认关闭内测模式 + "api_server_port": "8080", // 默认API端口 + "use_default_coins": "true", // 默认使用内置币种列表 + "default_coins": `["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]`, // 默认币种列表(JSON格式) + "max_daily_loss": "10.0", // 最大日损失百分比 + "max_drawdown": "20.0", // 最大回撤百分比 + "stop_trading_minutes": "60", // 停止交易时间(分钟) + "btc_eth_leverage": "5", // BTC/ETH杠杆倍数 + "altcoin_leverage": "5", // 山寨币杠杆倍数 + "jwt_secret": "", // JWT密钥,默认为空,由config.json或系统生成 } for key, value := range systemConfigs { @@ -416,7 +426,6 @@ type TraderRecord struct { TradingSymbols string `json:"trading_symbols"` // 交易币种,逗号分隔 UseCoinPool bool `json:"use_coin_pool"` // 是否使用COIN POOL信号源 UseOITop bool `json:"use_oi_top"` // 是否使用OI TOP信号源 - UseInsideCoins bool `json:"use_inside_coins"` // 是否使用内置评分信号源 CustomPrompt string `json:"custom_prompt"` // 自定义交易策略prompt OverrideBasePrompt bool `json:"override_base_prompt"` // 是否覆盖基础prompt SystemPromptTemplate string `json:"system_prompt_template"` // 系统提示词模板名称 @@ -772,9 +781,9 @@ func (d *Database) CreateExchange(userID, id, name, typ string, enabled bool, ap // CreateTrader 创建交易员 func (d *Database) 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, use_inside_coins, custom_prompt, override_base_prompt, system_prompt_template, is_cross_margin) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,?, ?, ?, ?, ?, ?) - `, 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.UseInsideCoins, trader.CustomPrompt, trader.OverrideBasePrompt, trader.SystemPromptTemplate, trader.IsCrossMargin) + 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, 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 } @@ -784,7 +793,7 @@ func (d *Database) GetTraders(userID string) ([]*TraderRecord, error) { 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, 0) as use_coin_pool, COALESCE(use_oi_top, 0) as use_oi_top,COALESCE(use_inside_coins, 0) as use_inside_coins, + COALESCE(use_coin_pool, 0) as use_coin_pool, COALESCE(use_oi_top, 0) as use_oi_top, COALESCE(custom_prompt, '') as custom_prompt, COALESCE(override_base_prompt, 0) as override_base_prompt, COALESCE(system_prompt_template, 'default') as system_prompt_template, COALESCE(is_cross_margin, 1) as is_cross_margin, created_at, updated_at @@ -802,7 +811,7 @@ func (d *Database) GetTraders(userID string) ([]*TraderRecord, error) { &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.UseInsideCoins, + &trader.UseCoinPool, &trader.UseOITop, &trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.SystemPromptTemplate, &trader.IsCrossMargin, &trader.CreatedAt, &trader.UpdatedAt, @@ -971,3 +980,105 @@ func (d *Database) GetCustomCoins() []string { func (d *Database) Close() error { return d.db.Close() } + +// LoadBetaCodesFromFile 从文件加载内测码到数据库 +func (d *Database) 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 OR IGNORE INTO beta_codes (code) VALUES (?)`) + 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 *Database) ValidateBetaCode(code string) (bool, error) { + var used bool + err := d.db.QueryRow(`SELECT used FROM beta_codes WHERE code = ?`, code).Scan(&used) + if err != nil { + if err == sql.ErrNoRows { + return false, nil // 内测码不存在 + } + return false, err + } + return !used, nil // 内测码存在且未使用 +} + +// UseBetaCode 使用内测码(标记为已使用) +func (d *Database) UseBetaCode(code, userEmail string) error { + result, err := d.db.Exec(` + UPDATE beta_codes SET used = 1, used_by = ?, used_at = CURRENT_TIMESTAMP + WHERE code = ? AND used = 0 + `, 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 *Database) 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 = 1`).Scan(&used) + if err != nil { + return 0, 0, err + } + + return total, used, nil +} diff --git a/docker-compose.yml b/docker-compose.yml index e2f6c905..a9d35026 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ services: volumes: - ./config.json:/app/config.json:ro - ./config.db:/app/config.db + - ./beta_codes.txt:/app/beta_codes.txt:ro - ./decision_logs:/app/decision_logs - ./prompts:/app/prompts - /etc/localtime:/etc/localtime:ro # Sync host time diff --git a/generate_beta_code.sh b/generate_beta_code.sh new file mode 100755 index 00000000..cee2ca3a --- /dev/null +++ b/generate_beta_code.sh @@ -0,0 +1,221 @@ +#!/bin/bash + +# 内测码生成脚本 +# 生成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 \ No newline at end of file diff --git a/go.mod b/go.mod index 067172fd..72291ee0 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,6 @@ require ( github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect github.com/elastic/go-sysinfo v1.15.4 // indirect github.com/elastic/go-windows v1.0.2 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect @@ -56,7 +55,6 @@ require ( github.com/prometheus/procfs v0.17.0 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/quic-go v0.54.0 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rs/zerolog v1.34.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sonirico/vago v0.9.0 // indirect @@ -80,8 +78,4 @@ require ( golang.org/x/tools v0.36.0 // indirect google.golang.org/protobuf v1.36.9 // indirect howett.net/plist v1.0.1 // indirect - modernc.org/libc v1.37.6 // indirect - modernc.org/mathutil v1.6.0 // indirect - modernc.org/memory v1.7.2 // indirect - modernc.org/sqlite v1.28.0 // indirect ) diff --git a/go.sum b/go.sum index a18e56af..655fcf92 100644 --- a/go.sum +++ b/go.sum @@ -32,7 +32,6 @@ 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/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= @@ -121,8 +120,6 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= -github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= @@ -147,7 +144,6 @@ 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/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= @@ -232,7 +228,3 @@ 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/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= -modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= -modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= -modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= diff --git a/main.go b/main.go index 36537b50..8aa83dde 100644 --- a/main.go +++ b/main.go @@ -26,12 +26,12 @@ type LeverageConfig struct { // ConfigFile 配置文件结构,只包含需要同步到数据库的字段 type ConfigFile struct { AdminMode bool `json:"admin_mode"` + BetaMode bool `json:"beta_mode"` APIServerPort int `json:"api_server_port"` UseDefaultCoins bool `json:"use_default_coins"` DefaultCoins []string `json:"default_coins"` CoinPoolAPIURL string `json:"coin_pool_api_url"` OITopAPIURL string `json:"oi_top_api_url"` - InsideCoins bool `json:"inside_coins"` MaxDailyLoss float64 `json:"max_daily_loss"` MaxDrawdown float64 `json:"max_drawdown"` StopTradingMinutes int `json:"stop_trading_minutes"` @@ -64,15 +64,15 @@ func syncConfigToDatabase(database *config.Database) error { // 同步各配置项到数据库 configs := map[string]string{ - "admin_mode": fmt.Sprintf("%t", configFile.AdminMode), - "api_server_port": strconv.Itoa(configFile.APIServerPort), - "use_default_coins": fmt.Sprintf("%t", configFile.UseDefaultCoins), - "coin_pool_api_url": configFile.CoinPoolAPIURL, - "oi_top_api_url": configFile.OITopAPIURL, - "inside_coins": fmt.Sprintf("%t", configFile.InsideCoins), - "max_daily_loss": fmt.Sprintf("%.1f", configFile.MaxDailyLoss), - "max_drawdown": fmt.Sprintf("%.1f", configFile.MaxDrawdown), - "stop_trading_minutes": strconv.Itoa(configFile.StopTradingMinutes), + "admin_mode": fmt.Sprintf("%t", configFile.AdminMode), + "beta_mode": fmt.Sprintf("%t", configFile.BetaMode), + "api_server_port": strconv.Itoa(configFile.APIServerPort), + "use_default_coins": fmt.Sprintf("%t", configFile.UseDefaultCoins), + "coin_pool_api_url": configFile.CoinPoolAPIURL, + "oi_top_api_url": configFile.OITopAPIURL, + "max_daily_loss": fmt.Sprintf("%.1f", configFile.MaxDailyLoss), + "max_drawdown": fmt.Sprintf("%.1f", configFile.MaxDrawdown), + "stop_trading_minutes": strconv.Itoa(configFile.StopTradingMinutes), } // 同步default_coins(转换为JSON字符串存储) @@ -109,6 +109,41 @@ func syncConfigToDatabase(database *config.Database) error { return nil } +// loadBetaCodesToDatabase 加载内测码文件到数据库 +func loadBetaCodesToDatabase(database *config.Database) error { + betaCodeFile := "beta_codes.txt" + + // 检查内测码文件是否存在 + if _, err := os.Stat(betaCodeFile); os.IsNotExist(err) { + log.Printf("📄 内测码文件 %s 不存在,跳过加载", betaCodeFile) + return nil + } + + // 获取文件信息 + fileInfo, err := os.Stat(betaCodeFile) + if err != nil { + return fmt.Errorf("获取内测码文件信息失败: %w", err) + } + + log.Printf("🔄 发现内测码文件 %s (%.1f KB),开始加载...", betaCodeFile, float64(fileInfo.Size())/1024) + + // 加载内测码到数据库 + err = database.LoadBetaCodesFromFile(betaCodeFile) + if err != nil { + return fmt.Errorf("加载内测码失败: %w", err) + } + + // 显示统计信息 + total, used, err := database.GetBetaCodeStats() + if err != nil { + log.Printf("⚠️ 获取内测码统计失败: %v", err) + } else { + log.Printf("✅ 内测码加载完成: 总计 %d 个,已使用 %d 个,剩余 %d 个", total, used, total-used) + } + + return nil +} + func main() { fmt.Println("╔════════════════════════════════════════════════════════════╗") fmt.Println("║ 🤖 AI多模型交易系统 - 支持 DeepSeek & Qwen ║") @@ -133,6 +168,11 @@ func main() { log.Printf("⚠️ 同步config.json到数据库失败: %v", err) } + // 加载内测码到数据库 + if err := loadBetaCodesToDatabase(database); err != nil { + log.Printf("⚠️ 加载内测码到数据库失败: %v", err) + } + // 获取系统配置 useDefaultCoinsStr, _ := database.GetSystemConfig("use_default_coins") useDefaultCoins := useDefaultCoinsStr == "true" diff --git a/manager/trader_manager.go b/manager/trader_manager.go index d3861cdb..4ebcf20b 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -1,27 +1,40 @@ package manager import ( + "context" "encoding/json" "fmt" "log" "nofx/config" "nofx/trader" + "sort" "strconv" "strings" "sync" "time" ) +// CompetitionCache 竞赛数据缓存 +type CompetitionCache struct { + data map[string]interface{} + timestamp time.Time + mu sync.RWMutex +} + // TraderManager 管理多个trader实例 type TraderManager struct { - traders map[string]*trader.AutoTrader // key: trader ID - mu sync.RWMutex + traders map[string]*trader.AutoTrader // key: trader ID + competitionCache *CompetitionCache + mu sync.RWMutex } // NewTraderManager 创建trader管理器 func NewTraderManager() *TraderManager { return &TraderManager{ traders: make(map[string]*trader.AutoTrader), + competitionCache: &CompetitionCache{ + data: make(map[string]interface{}), + }, } } @@ -84,7 +97,7 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro } // 为每个交易员获取AI模型和交易所配置 - for _, traderCfg := range allTraders { + for _, traderCfg := range allTraders { // 获取AI模型配置(使用交易员所属的用户ID) aiModels, err := database.GetAIModels(traderCfg.UserID) if err != nil { @@ -157,7 +170,7 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro } // 添加到TraderManager - err = tm.addTraderFromDB(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, oiTopURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, defaultCoins) + err = tm.addTraderFromDB(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, oiTopURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, defaultCoins) if err != nil { log.Printf("❌ 添加交易员 %s 失败: %v", traderCfg.Name, err) continue @@ -186,7 +199,7 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel } } } - + // 如果没有指定交易币种,使用默认币种 if len(tradingCoins) == 0 { tradingCoins = defaultCoins @@ -200,7 +213,7 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel } // 构建AutoTraderConfig - traderConfig := trader.AutoTraderConfig{ + traderConfig := trader.AutoTraderConfig{ ID: traderCfg.ID, Name: traderCfg.Name, AIModel: aiModelCfg.Provider, // 使用provider作为模型标识 @@ -253,7 +266,7 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel if err != nil { return fmt.Errorf("创建trader失败: %w", err) } - + // 设置自定义prompt(如果有) if traderCfg.CustomPrompt != "" { at.SetCustomPrompt(traderCfg.CustomPrompt) @@ -293,7 +306,7 @@ func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModel } } } - + // 如果没有指定交易币种,使用默认币种 if len(tradingCoins) == 0 { tradingCoins = defaultCoins @@ -359,7 +372,7 @@ func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModel if err != nil { return fmt.Errorf("创建trader失败: %w", err) } - + // 设置自定义prompt(如果有) if traderCfg.CustomPrompt != "" { at.SetCustomPrompt(traderCfg.CustomPrompt) @@ -478,59 +491,193 @@ func (tm *TraderManager) GetComparisonData() (map[string]interface{}, error) { // GetCompetitionData 获取竞赛数据(全平台所有交易员) func (tm *TraderManager) GetCompetitionData() (map[string]interface{}, error) { - tm.mu.RLock() - defer tm.mu.RUnlock() - - comparison := make(map[string]interface{}) - traders := make([]map[string]interface{}, 0) - - // 获取全平台所有交易员 - for _, t := range tm.traders { - account, err := t.GetAccountInfo() - status := t.GetStatus() - - var traderData map[string]interface{} - - if err != nil { - // 如果获取账户信息失败,使用默认值但仍然显示交易员 - log.Printf("⚠️ 获取交易员 %s 账户信息失败: %v", t.GetID(), err) - traderData = map[string]interface{}{ - "trader_id": t.GetID(), - "trader_name": t.GetName(), - "ai_model": t.GetAIModel(), - "exchange": t.GetExchange(), - "total_equity": 0.0, - "total_pnl": 0.0, - "total_pnl_pct": 0.0, - "position_count": 0, - "margin_used_pct": 0.0, - "is_running": status["is_running"], - "error": "账户数据获取失败", - } - } else { - // 正常情况下使用真实账户数据 - traderData = map[string]interface{}{ - "trader_id": t.GetID(), - "trader_name": t.GetName(), - "ai_model": t.GetAIModel(), - "exchange": t.GetExchange(), - "total_equity": account["total_equity"], - "total_pnl": account["total_pnl"], - "total_pnl_pct": account["total_pnl_pct"], - "position_count": account["position_count"], - "margin_used_pct": account["margin_used_pct"], - "is_running": status["is_running"], - } + // 检查缓存是否有效(30秒内) + tm.competitionCache.mu.RLock() + if time.Since(tm.competitionCache.timestamp) < 30*time.Second && len(tm.competitionCache.data) > 0 { + // 返回缓存数据 + cachedData := make(map[string]interface{}) + for k, v := range tm.competitionCache.data { + cachedData[k] = v } - - traders = append(traders, traderData) + tm.competitionCache.mu.RUnlock() + log.Printf("📋 返回竞赛数据缓存 (缓存时间: %.1fs)", time.Since(tm.competitionCache.timestamp).Seconds()) + return cachedData, nil } + tm.competitionCache.mu.RUnlock() + + tm.mu.RLock() + + // 获取所有交易员列表 + allTraders := make([]*trader.AutoTrader, 0, len(tm.traders)) + for _, t := range tm.traders { + allTraders = append(allTraders, t) + } + tm.mu.RUnlock() + + log.Printf("🔄 重新获取竞赛数据,交易员数量: %d", len(allTraders)) + + // 并发获取交易员数据 + traders := tm.getConcurrentTraderData(allTraders) + + // 按收益率排序(降序) + sort.Slice(traders, func(i, j int) bool { + pnlPctI, okI := traders[i]["total_pnl_pct"].(float64) + pnlPctJ, okJ := traders[j]["total_pnl_pct"].(float64) + if !okI { + pnlPctI = 0 + } + if !okJ { + pnlPctJ = 0 + } + return pnlPctI > pnlPctJ + }) + + // 限制返回前50名 + totalCount := len(traders) + limit := 50 + if len(traders) > limit { + traders = traders[:limit] + } + + comparison := make(map[string]interface{}) comparison["traders"] = traders comparison["count"] = len(traders) + comparison["total_count"] = totalCount // 总交易员数量 + + // 更新缓存 + tm.competitionCache.mu.Lock() + tm.competitionCache.data = comparison + tm.competitionCache.timestamp = time.Now() + tm.competitionCache.mu.Unlock() return comparison, nil } +// getConcurrentTraderData 并发获取多个交易员的数据 +func (tm *TraderManager) getConcurrentTraderData(traders []*trader.AutoTrader) []map[string]interface{} { + type traderResult struct { + index int + data map[string]interface{} + } + + // 创建结果通道 + resultChan := make(chan traderResult, len(traders)) + + // 并发获取每个交易员的数据 + for i, t := range traders { + go func(index int, trader *trader.AutoTrader) { + // 设置单个交易员的超时时间为3秒 + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + // 使用通道来实现超时控制 + accountChan := make(chan map[string]interface{}, 1) + errorChan := make(chan error, 1) + + go func() { + account, err := trader.GetAccountInfo() + if err != nil { + errorChan <- err + } else { + accountChan <- account + } + }() + + status := trader.GetStatus() + var traderData map[string]interface{} + + select { + case account := <-accountChan: + // 成功获取账户信息 + traderData = map[string]interface{}{ + "trader_id": trader.GetID(), + "trader_name": trader.GetName(), + "ai_model": trader.GetAIModel(), + "exchange": trader.GetExchange(), + "total_equity": account["total_equity"], + "total_pnl": account["total_pnl"], + "total_pnl_pct": account["total_pnl_pct"], + "position_count": account["position_count"], + "margin_used_pct": account["margin_used_pct"], + "is_running": status["is_running"], + } + case err := <-errorChan: + // 获取账户信息失败 + log.Printf("⚠️ 获取交易员 %s 账户信息失败: %v", trader.GetID(), err) + traderData = map[string]interface{}{ + "trader_id": trader.GetID(), + "trader_name": trader.GetName(), + "ai_model": trader.GetAIModel(), + "exchange": trader.GetExchange(), + "total_equity": 0.0, + "total_pnl": 0.0, + "total_pnl_pct": 0.0, + "position_count": 0, + "margin_used_pct": 0.0, + "is_running": status["is_running"], + "error": "账户数据获取失败", + } + case <-ctx.Done(): + // 超时 + log.Printf("⏰ 获取交易员 %s 账户信息超时", trader.GetID()) + traderData = map[string]interface{}{ + "trader_id": trader.GetID(), + "trader_name": trader.GetName(), + "ai_model": trader.GetAIModel(), + "exchange": trader.GetExchange(), + "total_equity": 0.0, + "total_pnl": 0.0, + "total_pnl_pct": 0.0, + "position_count": 0, + "margin_used_pct": 0.0, + "is_running": status["is_running"], + "error": "获取超时", + } + } + + resultChan <- traderResult{index: index, data: traderData} + }(i, t) + } + + // 收集所有结果 + results := make([]map[string]interface{}, len(traders)) + for i := 0; i < len(traders); i++ { + result := <-resultChan + results[result.index] = result.data + } + + return results +} + +// GetTopTradersData 获取前5名交易员数据(用于表现对比) +func (tm *TraderManager) GetTopTradersData() (map[string]interface{}, error) { + // 复用竞赛数据缓存,因为前5名是从全部数据中筛选出来的 + competitionData, err := tm.GetCompetitionData() + if err != nil { + return nil, err + } + + // 从竞赛数据中提取前5名 + allTraders, ok := competitionData["traders"].([]map[string]interface{}) + if !ok { + return nil, fmt.Errorf("竞赛数据格式错误") + } + + // 限制返回前5名 + limit := 5 + topTraders := allTraders + if len(allTraders) > limit { + topTraders = allTraders[:limit] + } + + result := map[string]interface{}{ + "traders": topTraders, + "count": len(topTraders), + } + + return result, nil +} + // isUserTrader 检查trader是否属于指定用户 func isUserTrader(traderID, userID string) bool { // trader ID格式: userID_traderName 或 randomUUID_modelName @@ -708,7 +855,7 @@ func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiMode } } } - + // 如果没有指定交易币种,使用默认币种 if len(tradingCoins) == 0 { tradingCoins = defaultCoins @@ -723,25 +870,25 @@ func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiMode // 构建AutoTraderConfig traderConfig := trader.AutoTraderConfig{ - ID: traderCfg.ID, - Name: traderCfg.Name, - AIModel: aiModelCfg.Provider, // 使用provider作为模型标识 - Exchange: exchangeCfg.ID, // 使用exchange ID - InitialBalance: traderCfg.InitialBalance, - BTCETHLeverage: traderCfg.BTCETHLeverage, - AltcoinLeverage: traderCfg.AltcoinLeverage, - ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute, - CoinPoolAPIURL: effectiveCoinPoolURL, - CustomAPIURL: aiModelCfg.CustomAPIURL, // 自定义API URL - CustomModelName: aiModelCfg.CustomModelName, // 自定义模型名称 - UseQwen: aiModelCfg.Provider == "qwen", - MaxDailyLoss: maxDailyLoss, - MaxDrawdown: maxDrawdown, - StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute, - IsCrossMargin: traderCfg.IsCrossMargin, - DefaultCoins: defaultCoins, - TradingCoins: tradingCoins, - SystemPromptTemplate: traderCfg.SystemPromptTemplate, // 系统提示词模板 + ID: traderCfg.ID, + Name: traderCfg.Name, + AIModel: aiModelCfg.Provider, // 使用provider作为模型标识 + Exchange: exchangeCfg.ID, // 使用exchange ID + InitialBalance: traderCfg.InitialBalance, + BTCETHLeverage: traderCfg.BTCETHLeverage, + AltcoinLeverage: traderCfg.AltcoinLeverage, + ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute, + CoinPoolAPIURL: effectiveCoinPoolURL, + CustomAPIURL: aiModelCfg.CustomAPIURL, // 自定义API URL + CustomModelName: aiModelCfg.CustomModelName, // 自定义模型名称 + UseQwen: aiModelCfg.Provider == "qwen", + MaxDailyLoss: maxDailyLoss, + MaxDrawdown: maxDrawdown, + StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute, + IsCrossMargin: traderCfg.IsCrossMargin, + DefaultCoins: defaultCoins, + TradingCoins: tradingCoins, + SystemPromptTemplate: traderCfg.SystemPromptTemplate, // 系统提示词模板 } // 根据交易所类型设置API密钥 @@ -769,7 +916,7 @@ func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiMode if err != nil { return fmt.Errorf("创建trader失败: %w", err) } - + // 设置自定义prompt(如果有) if traderCfg.CustomPrompt != "" { at.SetCustomPrompt(traderCfg.CustomPrompt) diff --git a/market/monitor.go b/market/monitor.go index 337640d8..23e126d9 100644 --- a/market/monitor.go +++ b/market/monitor.go @@ -106,8 +106,8 @@ func (m *WSMonitor) initializeHistoricalData() error { return } if len(klines4h) > 0 { - m.klineDataMap4h.Store(s, klines) - log.Printf("已加载 %s 的历史K线数据-4h: %d 条", s, len(klines)) + m.klineDataMap4h.Store(s, klines4h) + log.Printf("已加载 %s 的历史K线数据-4h: %d 条", s, len(klines4h)) } }(symbol) } diff --git a/trader/aster_trader.go b/trader/aster_trader.go index e4d7f12d..d9ba82a6 100644 --- a/trader/aster_trader.go +++ b/trader/aster_trader.go @@ -27,8 +27,8 @@ import ( // AsterTrader Aster交易平台实现 type AsterTrader struct { ctx context.Context - user string // 主钱包地址 (ERC20) - signer string // API钱包地址 + user string // 主钱包地址 (ERC20) + signer string // API钱包地址 privateKey *ecdsa.PrivateKey // API钱包私钥 client *http.Client baseURL string @@ -99,9 +99,9 @@ func (t *AsterTrader) getPrecision(symbol string) (SymbolPrecision, error) { body, _ := io.ReadAll(resp.Body) var info struct { Symbols []struct { - Symbol string `json:"symbol"` - PricePrecision int `json:"pricePrecision"` - QuantityPrecision int `json:"quantityPrecision"` + Symbol string `json:"symbol"` + PricePrecision int `json:"pricePrecision"` + QuantityPrecision int `json:"quantityPrecision"` Filters []map[string]interface{} `json:"filters"` } `json:"symbols"` } @@ -506,14 +506,14 @@ func (t *AsterTrader) GetPositions() ([]map[string]interface{}, error) { // 返回与Binance相同的字段名 result = append(result, map[string]interface{}{ - "symbol": pos["symbol"], - "side": side, - "positionAmt": posAmt, - "entryPrice": entryPrice, - "markPrice": markPrice, - "unRealizedProfit": unRealizedProfit, - "leverage": leverageVal, - "liquidationPrice": liquidationPrice, + "symbol": pos["symbol"], + "side": side, + "positionAmt": posAmt, + "entryPrice": entryPrice, + "markPrice": markPrice, + "unRealizedProfit": unRealizedProfit, + "leverage": leverageVal, + "liquidationPrice": liquidationPrice, }) } @@ -827,18 +827,18 @@ func (t *AsterTrader) SetMarginMode(symbol string, isCrossMargin bool) error { if !isCrossMargin { marginType = "ISOLATED" } - + params := map[string]interface{}{ "symbol": symbol, "marginType": marginType, } - + // 使用request方法调用API _, err := t.request("POST", "/fapi/v3/marginType", params) if err != nil { // 如果错误表示无需更改,忽略错误 - if strings.Contains(err.Error(), "No need to change") || - strings.Contains(err.Error(), "Margin type cannot be changed") { + if strings.Contains(err.Error(), "No need to change") || + strings.Contains(err.Error(), "Margin type cannot be changed") { log.Printf(" ✓ %s 仓位模式已是 %s 或有持仓无法更改", symbol, marginType) return nil } @@ -846,7 +846,7 @@ func (t *AsterTrader) SetMarginMode(symbol string, isCrossMargin bool) error { // 不返回错误,让交易继续 return nil } - + log.Printf(" ✓ %s 仓位模式已设置为 %s", symbol, marginType) return nil } diff --git a/trader/auto_trader.go b/trader/auto_trader.go index b23bb052..1e93ab5c 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -68,8 +68,8 @@ type AutoTraderConfig struct { IsCrossMargin bool // true=全仓模式, false=逐仓模式 // 币种配置 - DefaultCoins []string // 默认币种列表(从数据库获取) - TradingCoins []string // 实际交易币种列表 + DefaultCoins []string // 默认币种列表(从数据库获取) + TradingCoins []string // 实际交易币种列表 // 系统提示词模板 SystemPromptTemplate string // 系统提示词模板名称(如 "default", "aggressive") @@ -87,9 +87,9 @@ type AutoTrader struct { decisionLogger *logger.DecisionLogger // 决策日志记录器 initialBalance float64 dailyPnL float64 - customPrompt string // 自定义交易策略prompt - overrideBasePrompt bool // 是否覆盖基础prompt - systemPromptTemplate string // 系统提示词模板名称 + customPrompt string // 自定义交易策略prompt + overrideBasePrompt bool // 是否覆盖基础prompt + systemPromptTemplate string // 系统提示词模板名称 defaultCoins []string // 默认币种列表(从数据库获取) tradingCoins []string // 实际交易币种列表 lastResetTime time.Time @@ -1016,7 +1016,7 @@ func (at *AutoTrader) getCandidateCoins() ([]decision.CandidateCoin, error) { if len(at.tradingCoins) == 0 { // 使用数据库配置的默认币种列表 var candidateCoins []decision.CandidateCoin - + if len(at.defaultCoins) > 0 { // 使用数据库中配置的默认币种 for _, coin := range at.defaultCoins { @@ -1032,7 +1032,7 @@ func (at *AutoTrader) getCandidateCoins() ([]decision.CandidateCoin, error) { } else { // 如果数据库中没有配置默认币种,则使用AI500+OI Top作为fallback const ai500Limit = 20 // AI500取前20个评分最高的币种 - + mergedPool, err := pool.GetMergedCoinPool(ai500Limit) if err != nil { return nil, fmt.Errorf("获取合并币种池失败: %w", err) @@ -1073,11 +1073,11 @@ func (at *AutoTrader) getCandidateCoins() ([]decision.CandidateCoin, error) { func normalizeSymbol(symbol string) string { // 转为大写 symbol = strings.ToUpper(strings.TrimSpace(symbol)) - + // 确保以USDT结尾 if !strings.HasSuffix(symbol, "USDT") { symbol = symbol + "USDT" } - + return symbol } diff --git a/trader/binance_futures.go b/trader/binance_futures.go index c10fadeb..354415a0 100644 --- a/trader/binance_futures.go +++ b/trader/binance_futures.go @@ -139,18 +139,18 @@ func (t *FuturesTrader) SetMarginMode(symbol string, isCrossMargin bool) error { } else { marginType = futures.MarginTypeIsolated } - + // 尝试设置仓位模式 err := t.client.NewChangeMarginTypeService(). Symbol(symbol). MarginType(marginType). Do(context.Background()) - + marginModeStr := "全仓" if !isCrossMargin { marginModeStr = "逐仓" } - + if err != nil { // 如果错误信息包含"No need to change",说明仓位模式已经是目标值 if contains(err.Error(), "No need to change margin type") { @@ -166,7 +166,7 @@ func (t *FuturesTrader) SetMarginMode(symbol string, isCrossMargin bool) error { // 不返回错误,让交易继续 return nil } - + log.Printf(" ✓ %s 仓位模式已设置为 %s", symbol, marginModeStr) return nil } diff --git a/trader/hyperliquid_trader.go b/trader/hyperliquid_trader.go index 3073b342..c189dbdc 100644 --- a/trader/hyperliquid_trader.go +++ b/trader/hyperliquid_trader.go @@ -17,7 +17,7 @@ type HyperliquidTrader struct { ctx context.Context walletAddr string meta *hyperliquid.Meta // 缓存meta信息(包含精度等) - isCrossMargin bool // 是否为全仓模式 + isCrossMargin bool // 是否为全仓模式 } // NewHyperliquidTrader 创建Hyperliquid交易器 diff --git a/web/package-lock.json b/web/package-lock.json index a6afa248..08b930ea 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -72,6 +72,7 @@ "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", @@ -1275,6 +1276,7 @@ "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" @@ -1462,6 +1464,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -2157,6 +2160,7 @@ "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" } @@ -2477,6 +2481,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -2653,6 +2658,7 @@ "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" }, @@ -2664,6 +2670,7 @@ "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" @@ -3172,6 +3179,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -3286,6 +3294,7 @@ "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", @@ -3377,6 +3386,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, diff --git a/web/public/images/hand-bg.png b/web/public/images/hand-bg.png new file mode 100644 index 00000000..88988c6e Binary files /dev/null and b/web/public/images/hand-bg.png differ diff --git a/web/public/images/hand.png b/web/public/images/hand.png new file mode 100644 index 00000000..619da0ae Binary files /dev/null and b/web/public/images/hand.png differ diff --git a/web/public/images/logo.png b/web/public/images/logo.png deleted file mode 100644 index 28ec8c71..00000000 Binary files a/web/public/images/logo.png and /dev/null differ diff --git a/web/src/App.tsx b/web/src/App.tsx index 6f785908..14cf29d6 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -7,12 +7,12 @@ import { LoginPage } from './components/LoginPage'; import { RegisterPage } from './components/RegisterPage'; import { CompetitionPage } from './components/CompetitionPage'; import { LandingPage } from './pages/LandingPage'; +import HeaderBar from './components/landing/HeaderBar'; import AILearning from './components/AILearning'; import { LanguageProvider, useLanguage } from './contexts/LanguageContext'; import { AuthProvider, useAuth } from './contexts/AuthContext'; import { t, type Language } from './i18n/translations'; import { useSystemConfig } from './hooks/useSystemConfig'; -import { Zap } from 'lucide-react'; import type { SystemStatus, AccountInfo, @@ -44,29 +44,42 @@ function App() { const { config: systemConfig, loading: configLoading } = useSystemConfig(); const [route, setRoute] = useState(window.location.pathname); - // 从URL hash读取初始页面状态(支持刷新保持页面) + // 从URL路径读取初始页面状态(支持刷新保持页面) const getInitialPage = (): Page => { + const path = window.location.pathname; const hash = window.location.hash.slice(1); // 去掉 # - return hash === 'trader' || hash === 'details' ? 'trader' : 'competition'; + + if (path === '/traders' || hash === 'traders') return 'traders'; + if (path === '/dashboard' || hash === 'trader' || hash === 'details') return 'trader'; + return 'competition'; // 默认为竞赛页面 }; const [currentPage, setCurrentPage] = useState(getInitialPage()); const [selectedTraderId, setSelectedTraderId] = useState(); const [lastUpdate, setLastUpdate] = useState('--:--:--'); - // 监听URL hash变化,同步页面状态 + // 监听URL变化,同步页面状态 useEffect(() => { - const handleHashChange = () => { + const handleRouteChange = () => { + const path = window.location.pathname; const hash = window.location.hash.slice(1); - if (hash === 'trader' || hash === 'details') { + + if (path === '/traders' || hash === 'traders') { + setCurrentPage('traders'); + } else if (path === '/dashboard' || hash === 'trader' || hash === 'details') { setCurrentPage('trader'); - } else if (hash === 'competition' || hash === '') { + } else if (path === '/competition' || hash === 'competition' || hash === '') { setCurrentPage('competition'); } + setRoute(path); }; - window.addEventListener('hashchange', handleHashChange); - return () => window.removeEventListener('hashchange', handleHashChange); + window.addEventListener('hashchange', handleRouteChange); + window.addEventListener('popstate', handleRouteChange); + return () => { + window.removeEventListener('hashchange', handleRouteChange); + window.removeEventListener('popstate', handleRouteChange); + }; }, []); // 切换页面时更新URL hash (当前通过按钮直接调用setCurrentPage,这个函数暂时保留用于未来扩展) @@ -75,10 +88,14 @@ function App() { // window.location.hash = page === 'competition' ? '' : 'trader'; // }; - // 获取trader列表 - const { data: traders } = useSWR('traders', api.getTraders, { - refreshInterval: 10000, - }); + // 获取trader列表(仅在用户登录时) + const { data: traders } = useSWR( + user && token ? 'traders' : null, + api.getTraders, + { + refreshInterval: 10000, + } + ); // 当获取到traders后,设置默认选中第一个 useEffect(() => { @@ -166,153 +183,129 @@ function App() { return () => window.removeEventListener('popstate', handlePopState); }, []); + // Set current page based on route for consistent navigation state + useEffect(() => { + if (route === '/competition') { + setCurrentPage('competition'); + } else if (route === '/traders') { + setCurrentPage('traders'); + } else if (route === '/dashboard') { + setCurrentPage('trader'); + } + }, [route]); + // Show loading spinner while checking auth or config if (isLoading || configLoading) { return (
- NoFx Logo + NoFx Logo

{t('loading', language)}

); } - // Show landing page for root route when not authenticated + // Handle specific routes regardless of authentication + if (route === '/login') { + return ; + } + if (route === '/register') { + return ; + } + if (route === '/competition') { + return ( +
+ { + console.log('Competition page onPageChange called with:', page); + console.log('Current route:', route, 'Current page:', currentPage); + + if (page === 'competition') { + console.log('Navigating to competition'); + window.history.pushState({}, '', '/competition'); + setRoute('/competition'); + setCurrentPage('competition'); + } else if (page === 'traders') { + console.log('Navigating to traders'); + window.history.pushState({}, '', '/traders'); + setRoute('/traders'); + setCurrentPage('traders'); + } else if (page === 'trader') { + console.log('Navigating to trader/dashboard'); + window.history.pushState({}, '', '/dashboard'); + setRoute('/dashboard'); + setCurrentPage('trader'); + } + + console.log('After navigation - route:', route, 'currentPage:', currentPage); + }} + /> +
+ +
+
+ ); + } + + // Show landing page for root route + if (route === '/' || route === '') { + return ; + } + + // Show main app for authenticated users on other routes if (!systemConfig?.admin_mode && (!user || !token)) { - if (route === '/login') { - return ; - } - if (route === '/register') { - return ; - } - // Default to landing page when not authenticated + // Default to landing page when not authenticated and no specific route return ; } return (
- {/* Header - Binance Style */} -
-
-
- {/* Left - Logo and Title */} -
-
- NOFX -
-
-

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

-

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

-
-
- - {/* Center - Page Toggle (absolutely positioned) */} -
- - - -
- - {/* Right - Actions */} -
- - {/* User Info - Only show if not in admin mode */} - {!systemConfig?.admin_mode && user && ( -
-
- {user.email[0].toUpperCase()} -
- {user.email} -
- )} - - {/* Admin Mode Indicator */} - {systemConfig?.admin_mode && ( -
- - {t('adminMode', language)} -
- )} - - {/* Language Toggle */} -
- - -
- - {/* Logout Button - Only show if not in admin mode */} - {!systemConfig?.admin_mode && ( - - )} -
-
-
-
+ { + console.log('Main app onPageChange called with:', page); + + if (page === 'competition') { + window.history.pushState({}, '', '/competition'); + setRoute('/competition'); + setCurrentPage('competition'); + } else if (page === 'traders') { + window.history.pushState({}, '', '/traders'); + setRoute('/traders'); + setCurrentPage('traders'); + } else if (page === 'trader') { + window.history.pushState({}, '', '/dashboard'); + setRoute('/dashboard'); + setCurrentPage('trader'); + } + }} + /> {/* Main Content */} -
+
{currentPage === 'competition' ? ( ) : currentPage === 'traders' ? ( { setSelectedTraderId(traderId); + window.history.pushState({}, '', '/dashboard'); + setRoute('/dashboard'); setCurrentPage('trader'); }} /> diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx index 9361fc80..41b3cdc2 100644 --- a/web/src/components/AITradersPage.tsx +++ b/web/src/components/AITradersPage.tsx @@ -4,6 +4,7 @@ import { api } from '../lib/api'; import type { TraderInfo, CreateTraderRequest, AIModel, Exchange } from '../types'; import { useLanguage } from '../contexts/LanguageContext'; import { t, type Language } from '../i18n/translations'; +import { useAuth } from '../contexts/AuthContext'; import { getExchangeIcon } from './ExchangeIcons'; import { getModelIcon } from './ModelIcons'; import { TraderConfigModal } from './TraderConfigModal'; @@ -35,6 +36,7 @@ interface AITradersPageProps { export function AITradersPage({ onTraderSelect }: AITradersPageProps) { const { language } = useLanguage(); + const { user, token } = useAuth(); const [showCreateModal, setShowCreateModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [showModelModal, setShowModelModal] = useState(false); @@ -53,7 +55,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { }); const { data: traders, mutate: mutateTraders } = useSWR( - 'traders', + user && token ? 'traders' : null, api.getTraders, { refreshInterval: 5000 } ); @@ -61,6 +63,21 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { // 加载AI模型和交易所配置 useEffect(() => { const loadConfigs = async () => { + if (!user || !token) { + // 未登录时只加载公开的支持模型和交易所 + try { + const [supportedModels, supportedExchanges] = await Promise.all([ + api.getSupportedModels(), + api.getSupportedExchanges() + ]); + setSupportedModels(supportedModels); + setSupportedExchanges(supportedExchanges); + } catch (err) { + console.error('Failed to load supported configs:', err); + } + return; + } + try { const [modelConfigs, exchangeConfigs, supportedModels, supportedExchanges] = await Promise.all([ api.getModelConfigs(), @@ -88,7 +105,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { } }; loadConfigs(); - }, []); + }, [user, token]); // 显示所有用户的模型和交易所配置(用于调试) const configuredModels = allModels || []; @@ -183,6 +200,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { ai_model_id: data.ai_model_id, exchange_id: data.exchange_id, initial_balance: data.initial_balance, + scan_interval_minutes: data.scan_interval_minutes, btc_eth_leverage: data.btc_eth_leverage, altcoin_leverage: data.altcoin_leverage, trading_symbols: data.trading_symbols, @@ -457,22 +475,22 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { }; return ( -
+
{/* Header */} -
-
-
+
+
- +
-

+

{t('aiTraders', language)} - {traders?.length || 0} {t('active', language)} @@ -482,37 +500,37 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {

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

- +
+

+ {t('aiModels', language)}

-
+
{configuredModels.map(model => { const inUse = isModelInUse(model.id); return ( -
handleModelClick(model.id)} > -
-
- {getModelIcon(model.provider || model.id, { width: 32, height: 32 }) || ( -
+
+ {getModelIcon(model.provider || model.id, { width: 28, height: 28 }) || ( +
@@ -569,63 +587,63 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
)}
-
-
{getShortName(model.name)}
+
+
{getShortName(model.name)}
{inUse ? t('inUse', language) : model.enabled ? t('enabled', language) : t('configured', language)}
-
+
); })} {configuredModels.length === 0 && ( -
- -
{t('noModelsConfigured', language)}
+
+ +
{t('noModelsConfigured', language)}
)}
{/* Exchanges */} -
-

- +
+

+ {t('exchanges', language)}

-
+
{configuredExchanges.map(exchange => { const inUse = isExchangeInUse(exchange.id); return ( -
handleExchangeClick(exchange.id)} > -
-
- {getExchangeIcon(exchange.id, { width: 32, height: 32 })} +
+
+ {getExchangeIcon(exchange.id, { width: 28, height: 28 })}
-
-
{getShortName(exchange.name)}
+
+
{getShortName(exchange.name)}
{exchange.type.toUpperCase()} • {inUse ? t('inUse', language) : exchange.enabled ? t('enabled', language) : t('configured', language)}
-
+
); })} {configuredExchanges.length === 0 && ( -
- -
{t('noExchangesConfigured', language)}
+
+ +
{t('noExchangesConfigured', language)}
)}
@@ -633,47 +651,47 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
{/* Traders List */} -
-
-

- +
+
+

+ {t('currentTraders', language)}

{traders && traders.length > 0 ? ( -
+
{traders.map(trader => (
-
-
+
- +
-
-
+
+
{trader.trader_name}
-
{getModelDisplayName(trader.ai_model.split('_').pop() || trader.ai_model)} Model • {trader.exchange_id?.toUpperCase()}
-
+
{/* Status */}
{t('status', language)}
-
@@ -682,20 +700,20 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
{/* Actions */} -
+
- +
@@ -728,15 +746,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { ))}
) : ( -
- -
{t('noTraders', language)}
-
{t('createFirstTrader', language)}
+
+ +
{t('noTraders', language)}
+
{t('createFirstTrader', language)}
{(configuredModels.length === 0 || configuredExchanges.length === 0) && ( -
- {configuredModels.length === 0 && configuredExchanges.length === 0 +
+ {configuredModels.length === 0 && configuredExchanges.length === 0 ? t('configureModelsAndExchangesFirst', language) - : configuredModels.length === 0 + : configuredModels.length === 0 ? t('configureModelsFirst', language) : t('configureExchangesFirst', language) } diff --git a/web/src/components/ComparisonChart.tsx b/web/src/components/ComparisonChart.tsx index e8d1fafe..7a933920 100644 --- a/web/src/components/ComparisonChart.tsx +++ b/web/src/components/ComparisonChart.tsx @@ -31,11 +31,14 @@ export function ComparisonChart({ traders }: ComparisonChartProps) { const { data: allTraderHistories, isLoading } = useSWR( traders.length > 0 ? `all-equity-histories-${tradersKey}` : null, async () => { - // 并发请求所有trader的历史数据 - const promises = traders.map(trader => - api.getEquityHistory(trader.trader_id) - ); - return Promise.all(promises); + // 使用批量API一次性获取所有trader的历史数据 + const traderIds = traders.map(trader => trader.trader_id); + const batchData = await api.getEquityHistoryBatch(traderIds); + + // 转换为原格式,保持与原有代码兼容 + return traders.map(trader => { + return batchData.histories[trader.trader_id] || []; + }); }, { refreshInterval: 30000, // 30秒刷新(对比图表数据更新频率较低) @@ -89,8 +92,13 @@ export function ComparisonChart({ traders }: ComparisonChartProps) { }); } + // 计算盈亏百分比:从total_pnl和balance计算 + // 假设初始余额 = balance - total_pnl + const initialBalance = point.balance - point.total_pnl; + const pnlPct = initialBalance > 0 ? (point.total_pnl / initialBalance) * 100 : 0; + timestampMap.get(ts)!.traders.set(trader.trader_id, { - pnl_pct: point.total_pnl_pct, + pnl_pct: pnlPct, equity: point.total_equity }); }); @@ -225,7 +233,23 @@ export function ComparisonChart({ traders }: ComparisonChartProps) { return (
-
+
+ {/* NOFX Watermark */} +
+ NOFX +
@@ -313,24 +337,24 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
{/* Stats */} -
-
+
+
{t('comparisonMode', language)}
-
PnL %
+
PnL %
-
+
{t('dataPoints', language)}
-
{t('count', language, {count: combinedData.length})}
+
{t('count', language, {count: combinedData.length})}
-
+
{t('currentGap', language)}
-
1 ? '#F0B90B' : '#EAECEF' }}> +
1 ? '#F0B90B' : '#EAECEF' }}> {currentGap.toFixed(2)}%
-
+
{t('displayRange', language)}
-
+
{combinedData.length > MAX_DISPLAY_POINTS ? `${t('recent', language)} ${MAX_DISPLAY_POINTS}` : t('allData', language)} diff --git a/web/src/components/CompetitionPage.tsx b/web/src/components/CompetitionPage.tsx index 1ebdb564..d719472e 100644 --- a/web/src/components/CompetitionPage.tsx +++ b/web/src/components/CompetitionPage.tsx @@ -31,6 +31,8 @@ export function CompetitionPage() { setIsModalOpen(true); } catch (error) { console.error('Failed to fetch trader config:', error); + // 对于未登录用户,不显示详细配置,这是正常行为 + // 竞赛页面主要用于查看排行榜和基本信息 } }; @@ -39,7 +41,7 @@ export function CompetitionPage() { setSelectedTrader(null); }; - if (!competition || !competition.traders) { + if (!competition) { return (
@@ -62,6 +64,47 @@ export function CompetitionPage() { ); } + // 如果有数据返回但没有交易员,显示空状态 + if (!competition.traders || competition.traders.length === 0) { + return ( +
+ {/* Competition Header - 精简版 */} +
+
+
+ +
+
+

+ {t('aiCompetition', language)} + + 0 {t('traders', language)} + +

+

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

+
+
+
+ + {/* Empty State */} +
+ +

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

+

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

+
+
+ ); + } + // 按收益率排序 const sortedTraders = [...competition.traders].sort( (a, b) => b.total_pnl_pct - a.total_pnl_pct @@ -73,13 +116,16 @@ export function CompetitionPage() { return (
{/* Competition Header - 精简版 */} -
-
-
- +
+
+
+
-

+

{t('aiCompetition', language)} {competition.count} {t('traders', language)} @@ -90,9 +136,9 @@ export function CompetitionPage() {

-
+
{t('leader', language)}
-
{leader?.trader_name}
+
{leader?.trader_name}
= 0 ? '#0ECB81' : '#F6465D' }}> {(leader?.total_pnl ?? 0) >= 0 ? '+' : ''}{leader?.total_pnl_pct?.toFixed(2) || '0.00'}%
@@ -111,7 +157,7 @@ export function CompetitionPage() { {t('realTimePnL', language)}
- +
{/* Right: Leaderboard */} @@ -155,20 +201,20 @@ export function CompetitionPage() {
{/* Stats */} -
+
{/* Total Equity */}
{t('equity', language)}
-
+
{trader.total_equity?.toFixed(2) || '0.00'}
{/* P&L */} -
+
{t('pnl', language)}
= 0 ? '#0ECB81' : '#F6465D' }} > {(trader.total_pnl ?? 0) >= 0 ? '+' : ''} @@ -182,7 +228,7 @@ export function CompetitionPage() { {/* Positions */}
{t('pos', language)}
-
+
{trader.position_count}
@@ -242,15 +288,12 @@ export function CompetitionPage() { >
{trader.trader_name}
-
- {trader.ai_model.toUpperCase()} + {trader.exchange.toUpperCase()} -
-
= 0 ? '#0ECB81' : '#F6465D' }}> +
= 0 ? '#0ECB81' : '#F6465D' }}> {(trader.total_pnl ?? 0) >= 0 ? '+' : ''}{trader.total_pnl_pct?.toFixed(2) || '0.00'}%
{isWinning && gap > 0 && ( diff --git a/web/src/components/CryptoFeatureCard.tsx b/web/src/components/CryptoFeatureCard.tsx index 9c78960a..0affa99d 100644 --- a/web/src/components/CryptoFeatureCard.tsx +++ b/web/src/components/CryptoFeatureCard.tsx @@ -30,8 +30,8 @@ export const CryptoFeatureCard = React.forwardRef {/* Icon container */} -
{icon}
+
{icon}
{/* Title */} -

{title}

+

{title}

{/* Description */} -

{description}

+

{description}

{/* Features list */}
@@ -95,11 +95,11 @@ export const CryptoFeatureCard = React.forwardRef
-
- +
+
- {feature} + {feature} ))}
diff --git a/web/src/components/EquityChart.tsx b/web/src/components/EquityChart.tsx index bfa89617..e7441779 100644 --- a/web/src/components/EquityChart.tsx +++ b/web/src/components/EquityChart.tsx @@ -270,7 +270,23 @@ export function EquityChart({ traderId }: EquityChartProps) {
{/* Chart */} -
+
+ {/* NOFX Watermark */} +
+ NOFX +
- NoFx Logo + NoFx Logo

diff --git a/web/src/components/LoginPage.tsx b/web/src/components/LoginPage.tsx index ea36356c..a1ed3512 100644 --- a/web/src/components/LoginPage.tsx +++ b/web/src/components/LoginPage.tsx @@ -2,8 +2,7 @@ import React, { useState } from 'react'; import { useAuth } from '../contexts/AuthContext'; import { useLanguage } from '../contexts/LanguageContext'; import { t } from '../i18n/translations'; -import { Header } from './Header'; -import { ArrowLeft } from 'lucide-react'; +import HeaderBar from './landing/HeaderBar'; export function LoginPage() { const { language } = useLanguage(); @@ -51,43 +50,44 @@ export function LoginPage() { }; return ( -
-
+
+ {}} + isLoggedIn={false} + isHomePage={false} + currentPage="login" + language={language} + onLanguageChange={() => {}} + onPageChange={(page) => { + console.log('LoginPage onPageChange called with:', page); + if (page === 'competition') { + window.location.href = '/competition'; + } + }} + /> -
+
- {/* Back to Home */} - {/* Logo */}
- NoFx Logo + NoFx Logo
-

- {t('loginTitle', language)} +

+ 登录 NOFX

-

- {step === 'login' ? t('loginTitle', language) : t('enterOTPCode', language)} +

+ {step === 'login' ? '请输入您的邮箱和密码' : '请输入两步验证码'}

{/* Login Form */} -
+
{step === 'login' ? (
-
-
{error && ( -
+
{error}
)} @@ -126,7 +126,7 @@ export function LoginPage() { type="submit" disabled={loading} className="w-full px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50" - style={{ background: '#F0B90B', color: '#000' }} + style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }} > {loading ? t('loading', language) : t('loginButton', language)} @@ -142,7 +142,7 @@ export function LoginPage() {
-
{error && ( -
+
{error}
)} @@ -168,7 +168,7 @@ export function LoginPage() { type="button" onClick={() => setStep('login')} className="flex-1 px-4 py-2 rounded text-sm font-semibold" - style={{ background: '#2B3139', color: '#848E9C' }} + style={{ background: 'var(--panel-bg-hover)', color: 'var(--text-secondary)' }} > {t('back', language)} @@ -187,17 +187,17 @@ export function LoginPage() { {/* Register Link */}
-

- {t('noAccount', language)}{' '} +

+ 还没有账户?{' '}

diff --git a/web/src/components/RegisterPage.tsx b/web/src/components/RegisterPage.tsx index 438bed05..4fdbcace 100644 --- a/web/src/components/RegisterPage.tsx +++ b/web/src/components/RegisterPage.tsx @@ -1,8 +1,9 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { useAuth } from '../contexts/AuthContext'; import { useLanguage } from '../contexts/LanguageContext'; import { t } from '../i18n/translations'; -import { ArrowLeft } from 'lucide-react'; +import { getSystemConfig } from '../lib/config'; +import HeaderBar from './landing/HeaderBar'; export function RegisterPage() { const { language } = useLanguage(); @@ -11,6 +12,8 @@ export function RegisterPage() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); + const [betaCode, setBetaCode] = useState(''); + const [betaMode, setBetaMode] = useState(false); const [otpCode, setOtpCode] = useState(''); const [userID, setUserID] = useState(''); const [otpSecret, setOtpSecret] = useState(''); @@ -18,6 +21,15 @@ export function RegisterPage() { const [error, setError] = useState(''); const [loading, setLoading] = useState(false); + useEffect(() => { + // 获取系统配置,检查是否开启内测模式 + getSystemConfig().then(config => { + setBetaMode(config.beta_mode || false); + }).catch(err => { + console.error('Failed to fetch system config:', err); + }); + }, []); + const handleRegister = async (e: React.FormEvent) => { e.preventDefault(); setError(''); @@ -32,9 +44,14 @@ export function RegisterPage() { return; } + if (betaMode && !betaCode.trim()) { + setError('内测期间,注册需要提供内测码'); + return; + } + setLoading(true); - const result = await register(email, password); + const result = await register(email, password, betaCode.trim() || undefined); if (result.success && result.userID) { setUserID(result.userID); @@ -72,27 +89,28 @@ export function RegisterPage() { }; return ( -
-
- {/* Back to Home */} - {step === 'register' && ( - - )} +
+ {}} + onPageChange={(page) => { + console.log('RegisterPage onPageChange called with:', page); + if (page === 'competition') { + window.location.href = '/competition'; + } + }} + /> - {/* Logo */} -
+
+
+ + {/* Logo */} +
- NoFx Logo + NoFx Logo

{t('appTitle', language)} @@ -105,11 +123,11 @@ export function RegisterPage() {

{/* Registration Form */} -
+
{step === 'register' && (
-
-
-
+ {betaMode && ( +
+ + setBetaCode(e.target.value.replace(/[^a-z0-9]/gi, '').toLowerCase())} + className="w-full px-3 py-2 rounded font-mono" + style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} + placeholder="请输入6位内测码" + maxLength={6} + required={betaMode} + /> +

+ 内测码由6位字母数字组成,区分大小写 +

+
+ )} + {error && ( -
+
{error}
)} @@ -183,21 +222,21 @@ export function RegisterPage() {
-
-

- {t('step1Title', language)} +

+

+ {t('authStep1Title', language)}

-

- {t('step1Desc', language)} +

+ {t('authStep1Desc', language)}

-
-

- {t('step2Title', language)} +

+

+ {t('authStep2Title', language)}

- {t('step2Desc', language)} + {t('authStep2Desc', language)}

{qrCodeURL && ( @@ -214,13 +253,13 @@ export function RegisterPage() {

{t('otpSecret', language)}

+ style={{ background: 'var(--panel-bg-hover)', color: 'var(--brand-light-gray)' }}> {otpSecret} @@ -228,12 +267,12 @@ export function RegisterPage() {
-
-

- {t('step3Title', language)} +

+

+ {t('authStep3Title', language)}

-

- {t('step3Desc', language)} +

+ {t('authStep3Desc', language)}

@@ -259,7 +298,7 @@ export function RegisterPage() {
-
{error && ( -
+
{error}
)} @@ -285,7 +324,7 @@ export function RegisterPage() { type="button" onClick={() => setStep('setup-otp')} className="flex-1 px-4 py-2 rounded text-sm font-semibold" - style={{ background: '#2B3139', color: '#848E9C' }} + style={{ background: 'var(--panel-bg-hover)', color: 'var(--text-secondary)' }} > {t('back', language)} @@ -305,21 +344,22 @@ export function RegisterPage() { {/* Login Link */} {step === 'register' && (
-

+

已有账户?{' '}

)} +
); diff --git a/web/src/components/TraderConfigModal.tsx b/web/src/components/TraderConfigModal.tsx index 528f763f..4676c194 100644 --- a/web/src/components/TraderConfigModal.tsx +++ b/web/src/components/TraderConfigModal.tsx @@ -1,5 +1,7 @@ import { useState, useEffect } from 'react'; import type { AIModel, Exchange, CreateTraderRequest } from '../types'; +import { useLanguage } from '../contexts/LanguageContext'; +import { t } from '../i18n/translations'; // 提取下划线后面的名称部分 function getShortName(fullName: string): string { @@ -22,6 +24,7 @@ interface TraderConfigData { use_coin_pool: boolean; use_oi_top: boolean; initial_balance: number; + scan_interval_minutes: number; } interface TraderConfigModalProps { @@ -43,6 +46,7 @@ export function TraderConfigModal({ availableExchanges = [], onSave }: TraderConfigModalProps) { + const { language } = useLanguage(); const [formData, setFormData] = useState({ trader_name: '', ai_model: '', @@ -57,6 +61,7 @@ export function TraderConfigModal({ use_coin_pool: false, use_oi_top: false, initial_balance: 1000, + scan_interval_minutes: 3, }); const [isSaving, setIsSaving] = useState(false); const [availableCoins, setAvailableCoins] = useState([]); @@ -87,6 +92,7 @@ export function TraderConfigModal({ use_coin_pool: false, use_oi_top: false, initial_balance: 1000, + scan_interval_minutes: 3, }); } // 确保旧数据也有默认的 system_prompt_template @@ -181,6 +187,7 @@ export function TraderConfigModal({ use_coin_pool: formData.use_coin_pool, use_oi_top: formData.use_oi_top, initial_balance: formData.initial_balance, + scan_interval_minutes: formData.scan_interval_minutes, }; await onSave(saveData); onClose(); @@ -319,7 +326,25 @@ export function TraderConfigModal({
- {/* 第二行:杠杆设置 */} + {/* 第二行:AI 扫描决策间隔 */} +
+
+ + handleInputChange('scan_interval_minutes', Number(e.target.value))} + className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none" + min="1" + max="60" + step="1" + /> +

{t('scanIntervalRecommend', language)}

+
+
+
+ + {/* 第三行:杠杆设置 */}
diff --git a/web/src/components/landing/AboutSection.tsx b/web/src/components/landing/AboutSection.tsx index d087a66f..55ff9d9b 100644 --- a/web/src/components/landing/AboutSection.tsx +++ b/web/src/components/landing/AboutSection.tsx @@ -2,8 +2,13 @@ import { motion } from 'framer-motion' import { Shield, Target } from 'lucide-react' import AnimatedSection from './AnimatedSection' import Typewriter from '../Typewriter' +import { t, Language } from '../../i18n/translations' -export default function AboutSection() { +interface AboutSectionProps { + language: Language +} + +export default function AboutSection({ language }: AboutSectionProps) { return (
@@ -31,7 +36,7 @@ export default function AboutSection() { className='text-sm font-semibold' style={{ color: 'var(--brand-yellow)' }} > - 关于 NOFX + {t('aboutNofx', language)} @@ -39,23 +44,19 @@ export default function AboutSection() { className='text-4xl font-bold' style={{ color: 'var(--brand-light-gray)' }} > - 什么是 NOFX? + {t('whatIsNofx', language)}

- NOFX 不是另一个交易机器人,而是 AI 交易的 'Linux' —— - 一个透明、可信任的开源 OS,提供统一的 '决策-风险-执行' - 层,支持所有资产类别。 + {t('nofxNotAnotherBot', language)} {t('nofxDescription1', language)} {t('nofxDescription2', language)}

- 从加密市场起步(24/7、高波动性完美测试场),未来扩展到股票、期货、外汇。核心:开放架构、AI - 达尔文主义(多代理自竞争、策略进化)、CodeFi 飞轮(开发者 PR - 贡献获积分奖励)。 + {t('nofxDescription3', language)} {t('nofxDescription4', language)} {t('nofxDescription5', language)}

- 你 100% 掌控 + {t('youFullControl', language)}
- 完全掌控 AI 提示词和资金 + {t('fullControlDesc', language)}
@@ -101,16 +102,16 @@ export default function AboutSection() { '$ cd nofx', '$ chmod +x start.sh', '$ ./start.sh start --build', - ' 启动自动交易系统...', - ' API服务器启动在端口 8080', - ' Web 控制台 http://localhost:3000', + t('startupMessages1', language), + t('startupMessages2', language), + t('startupMessages3', language), ]} typingSpeed={70} lineDelay={900} className='text-sm font-mono' style={{ - color: '#00FF41', - textShadow: '0 0 6px rgba(0,255,65,0.6)', + color: '#00FF88', + textShadow: '0 0 8px rgba(0,255,136,0.4)', }} />
diff --git a/web/src/components/landing/CommunitySection.tsx b/web/src/components/landing/CommunitySection.tsx index 9edbf4d4..0e52d926 100644 --- a/web/src/components/landing/CommunitySection.tsx +++ b/web/src/components/landing/CommunitySection.tsx @@ -1,7 +1,16 @@ import { motion } from 'framer-motion' import AnimatedSection from './AnimatedSection' -function TestimonialCard({ quote, author, delay }: any) { +interface CardProps { + quote: string; + authorName: string; + handle: string; + avatarUrl: string; + tweetUrl: string; + delay: number; +} + +function TestimonialCard({ quote, authorName, delay }: CardProps) { return (
- {author} + {authorName}
diff --git a/web/src/components/landing/FeaturesSection.tsx b/web/src/components/landing/FeaturesSection.tsx index 7eef7e00..3026405f 100644 --- a/web/src/components/landing/FeaturesSection.tsx +++ b/web/src/components/landing/FeaturesSection.tsx @@ -2,8 +2,13 @@ import { motion } from 'framer-motion' import AnimatedSection from './AnimatedSection' import { CryptoFeatureCard } from '../CryptoFeatureCard' import { Code, Cpu, Lock, Rocket } from 'lucide-react' +import { t, Language } from '../../i18n/translations' -export default function FeaturesSection() { +interface FeaturesSectionProps { + language: Language +} + +export default function FeaturesSection({ language }: FeaturesSectionProps) { return (
@@ -15,37 +20,52 @@ export default function FeaturesSection() { > - 核心功能 + {t('coreFeatures', language)}

- 为什么选择 NOFX? + {t('whyChooseNofx', language)}

- 开源、透明、社区驱动的 AI 交易操作系统 + {t('openCommunityDriven', language)}

} - title='100% 开源与自托管' - description='你的框架,你的规则。非黑箱,支持自定义提示词和多模型。' - features={['完全开源代码', '支持自托管部署', '自定义 AI 提示词', '多模型支持(DeepSeek、Qwen)']} + title={t('openSourceSelfHosted', language)} + description={t('openSourceDesc', language)} + features={[ + t('openSourceFeatures1', language), + t('openSourceFeatures2', language), + t('openSourceFeatures3', language), + t('openSourceFeatures4', language) + ]} delay={0} /> } - title='多代理智能竞争' - description='AI 策略在沙盒中高速战斗,最优者生存,实现策略进化。' - features={['多 AI 代理并行运行', '策略自动优化', '沙盒安全测试', '跨市场策略移植']} + title={t('multiAgentCompetition', language)} + description={t('multiAgentDesc', language)} + features={[ + t('multiAgentFeatures1', language), + t('multiAgentFeatures2', language), + t('multiAgentFeatures3', language), + t('multiAgentFeatures4', language) + ]} delay={0.1} /> } - title='安全可靠交易' - description='企业级安全保障,完全掌控你的资金和交易策略。' - features={['本地私钥管理', 'API 权限精细控制', '实时风险监控', '交易日志审计']} + title={t('secureReliableTrading', language)} + description={t('secureDesc', language)} + features={[ + t('secureFeatures1', language), + t('secureFeatures2', language), + t('secureFeatures3', language), + t('secureFeatures4', language) + ]} delay={0.2} />
diff --git a/web/src/components/landing/FooterSection.tsx b/web/src/components/landing/FooterSection.tsx index 6ff3faa9..598409fe 100644 --- a/web/src/components/landing/FooterSection.tsx +++ b/web/src/components/landing/FooterSection.tsx @@ -1,20 +1,22 @@ -import { useLanguage } from '../../contexts/LanguageContext' -import { t } from '../../i18n/translations' +import { t, Language } from '../../i18n/translations' -export default function FooterSection() { - const { language } = useLanguage() +interface FooterSectionProps { + language: Language +} + +export default function FooterSection({ language }: FooterSectionProps) { return ( -
+
{/* Brand */}
- NOFX Logo + NOFX Logo
NOFX
- AI 交易的未来标准 + {t('futureStandardAI', language)}
@@ -26,7 +28,7 @@ export default function FooterSection() { className='text-sm font-semibold mb-3' style={{ color: '#EAECEF' }} > - 链接 + {t('links', language)}

  • @@ -67,7 +69,7 @@ export default function FooterSection() { className='text-sm font-semibold mb-3' style={{ color: '#EAECEF' }} > - 资源 + {t('resources', language)}

  • @@ -77,7 +79,7 @@ export default function FooterSection() { target='_blank' rel='noopener noreferrer' > - 文档 + {t('documentation', language)}
  • @@ -108,7 +110,7 @@ export default function FooterSection() { className='text-sm font-semibold mb-3' style={{ color: '#EAECEF' }} > - 支持方 + {t('supporters', language)}

  • @@ -148,7 +150,7 @@ export default function FooterSection() { target='_blank' rel='noopener noreferrer' > - Amber.ac (战略投资) + Amber.ac {t('strategicInvestment', language)}
@@ -158,7 +160,7 @@ export default function FooterSection() { {/* Bottom note (kept subtle) */}

{t('footerTitle', language)}

{t('footerWarning', language)}

diff --git a/web/src/components/landing/HeaderBar.tsx b/web/src/components/landing/HeaderBar.tsx index b3f10049..41db209b 100644 --- a/web/src/components/landing/HeaderBar.tsx +++ b/web/src/components/landing/HeaderBar.tsx @@ -1,56 +1,363 @@ -import { useState } from 'react' +import { useState, useEffect, useRef } from 'react' import { motion } from 'framer-motion' -import { Menu, X } from 'lucide-react' +import { Menu, X, ChevronDown } from 'lucide-react' +import { t, type Language } from '../../i18n/translations' -export default function HeaderBar({ onLoginClick }: { onLoginClick: () => void }) { +interface HeaderBarProps { + onLoginClick?: () => void + isLoggedIn?: boolean + isHomePage?: boolean + currentPage?: string + language?: Language + onLanguageChange?: (lang: Language) => void + user?: { email: string } | null + onLogout?: () => void + isAdminMode?: boolean + onPageChange?: (page: string) => void +} + +export default function HeaderBar({ isLoggedIn = false, isHomePage = false, currentPage, language = 'zh' as Language, onLanguageChange, user, onLogout, isAdminMode = false, 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/components/landing/HeroSection.tsx b/web/src/components/landing/HeroSection.tsx index 61de5e7a..52656bf6 100644 --- a/web/src/components/landing/HeroSection.tsx +++ b/web/src/components/landing/HeroSection.tsx @@ -1,10 +1,16 @@ -import { motion, useScroll, useTransform } from 'framer-motion' +import { motion, useScroll, useTransform, useAnimation } from 'framer-motion' import { Sparkles } from 'lucide-react' +import { t, Language } from '../../i18n/translations' -export default function HeroSection() { +interface HeroSectionProps { + language: Language +} + +export default function HeroSection({ language }: HeroSectionProps) { const { scrollYProgress } = useScroll() const opacity = useTransform(scrollYProgress, [0, 0.2], [1, 0]) const scale = useTransform(scrollYProgress, [0, 0.2], [1, 0.8]) + const handControls = useAnimation() const fadeInUp = { initial: { opacity: 0, y: 60 }, @@ -27,40 +33,39 @@ export default function HeroSection() { > - 3 天内 2.5K+ GitHub Stars +{t('githubStarsInDays', language)}

- Read the Market. + {t('heroTitle1', language)}
- Write the Trade. + {t('heroTitle2', language)}

- NOFX 是 AI 交易的未来标准——一个开放、社区驱动的代理式交易操作系统。支持 Binance、Aster DEX 等交易所, - 自托管、多代理竞争,让 AI 为你自动决策、执行和优化交易。 + {t('heroDescription', language)}
GitHub Stars GitHub Forks GitHub Contributors @@ -68,12 +73,62 @@ export default function HeroSection() {
- 由 Aster DEX 和 Binance 提供支持,Amber.ac 战略投资。 +{t('poweredBy', language)} - {/* Right Visual */} - + {/* Right Visual - Interactive Robot */} +
{ + handControls.start({ + y: [-8, 8, -8], + rotate: [-3, 3, -3], + x: [-2, 2, -2], + transition: { + duration: 2.5, + repeat: Infinity, + ease: "easeInOut", + times: [0, 0.5, 1] + } + }) + }} + onMouseLeave={() => { + handControls.start({ + y: 0, + rotate: 0, + x: 0, + transition: { + duration: 0.6, + ease: "easeOut" + } + }) + }} + > + {/* Background Layer */} + + + {/* Hand Layer - Animated */} + +
diff --git a/web/src/components/landing/HowItWorksSection.tsx b/web/src/components/landing/HowItWorksSection.tsx index f33075e7..4fefa15d 100644 --- a/web/src/components/landing/HowItWorksSection.tsx +++ b/web/src/components/landing/HowItWorksSection.tsx @@ -1,5 +1,6 @@ import { motion } from 'framer-motion' import AnimatedSection from './AnimatedSection' +import { t, Language } from '../../i18n/translations' function StepCard({ number, title, description, delay }: any) { return ( @@ -24,25 +25,29 @@ function StepCard({ number, title, description, delay }: any) { ) } -export default function HowItWorksSection() { +interface HowItWorksSectionProps { + language: Language +} + +export default function HowItWorksSection({ language }: HowItWorksSectionProps) { return (

- 如何开始使用 NOFX + {t('howToStart', language)}

- 四个简单步骤,开启 AI 自动交易之旅 + {t('fourSimpleSteps', language)}

{[ - { number: 1, title: '拉取 GitHub 仓库', description: 'git clone https://github.com/tinkle-community/nofx 并切换到 dev 分支测试新功能。' }, - { number: 2, title: '配置环境', description: '前端设置交易所 API(如 Binance、Hyperliquid)、AI 模型和自定义提示词。' }, - { number: 3, title: '部署与运行', description: '一键 Docker 部署,启动 AI 代理。注意:高风险市场,仅用闲钱测试。' }, - { number: 4, title: '优化与贡献', description: '监控交易,提交 PR 改进框架。加入 Telegram 分享策略。' }, + { number: 1, title: t('step1Title', language), description: t('step1Desc', language) }, + { number: 2, title: t('step2Title', language), description: t('step2Desc', language) }, + { number: 3, title: t('step3Title', language), description: t('step3Desc', language) }, + { number: 4, title: t('step4Title', language), description: t('step4Desc', language) }, ].map((step, index) => ( ))} @@ -61,10 +66,10 @@ export default function HowItWorksSection() {
- 重要风险提示 + {t('importantRiskWarning', language)}

- dev 分支不稳定,勿用无法承受损失的资金。NOFX 非托管,无官方策略。交易有风险,投资需谨慎。 + {t('riskWarningText', language)}

diff --git a/web/src/components/landing/LoginModal.tsx b/web/src/components/landing/LoginModal.tsx index c981b506..b5bdd58d 100644 --- a/web/src/components/landing/LoginModal.tsx +++ b/web/src/components/landing/LoginModal.tsx @@ -1,7 +1,13 @@ import { motion } from 'framer-motion' import { X } from 'lucide-react' +import { t, Language } from '../../i18n/translations' -export default function LoginModal({ onClose }: { onClose: () => void }) { +interface LoginModalProps { + onClose: () => void + language: Language +} + +export default function LoginModal({ onClose, language }: LoginModalProps) { return ( void }) {

- 访问 NOFX 平台 + {t('accessNofxPlatform', language)}

- 请选择登录或注册以访问完整的 AI 交易平台 + {t('loginRegisterPrompt', language)}

void }) { whileHover={{ scale: 1.05, boxShadow: '0 10px 30px rgba(240, 185, 11, 0.4)' }} whileTap={{ scale: 0.95 }} > - 登录 + {t('signIn', language)} { @@ -53,7 +59,7 @@ export default function LoginModal({ onClose }: { onClose: () => void }) { whileHover={{ scale: 1.05, borderColor: 'var(--brand-yellow)' }} whileTap={{ scale: 0.95 }} > - 注册新账号 + {t('registerNewAccount', language)}
diff --git a/web/src/contexts/AuthContext.tsx b/web/src/contexts/AuthContext.tsx index 9b922751..cecdd953 100644 --- a/web/src/contexts/AuthContext.tsx +++ b/web/src/contexts/AuthContext.tsx @@ -10,7 +10,7 @@ interface AuthContextType { user: User | null; token: string | null; login: (email: string, password: string) => Promise<{ success: boolean; message?: string; userID?: string; requiresOTP?: boolean }>; - register: (email: string, password: string) => Promise<{ success: boolean; message?: string; userID?: string; otpSecret?: string; qrCodeURL?: string }>; + register: (email: string, password: string, betaCode?: string) => Promise<{ success: boolean; message?: string; userID?: string; otpSecret?: string; qrCodeURL?: string }>; verifyOTP: (userID: string, otpCode: string) => Promise<{ success: boolean; message?: string }>; completeRegistration: (userID: string, otpCode: string) => Promise<{ success: boolean; message?: string }>; logout: () => void; @@ -89,14 +89,19 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { return { success: false, message: '未知错误' }; }; - const register = async (email: string, password: string) => { + const register = async (email: string, password: string, betaCode?: string) => { try { + const requestBody: { email: string; password: string; beta_code?: string } = { email, password }; + if (betaCode) { + requestBody.beta_code = betaCode; + } + const response = await fetch('/api/register', { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ email, password }), + body: JSON.stringify(requestBody), }); const data = await response.json(); diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts index ed5caaad..99a11cac 100644 --- a/web/src/i18n/translations.ts +++ b/web/src/i18n/translations.ts @@ -15,6 +15,11 @@ export const translations = { logout: 'Logout', switchTrader: 'Switch Trader:', view: 'View', + + // Navigation + realtimeNav: 'Live', + configNav: 'Config', + dashboardNav: 'Dashboard', // Footer footerTitle: 'NOFX - AI Trading System', @@ -158,6 +163,8 @@ export const translations = { create: 'Create', configureAIModels: 'Configure AI Models', configureExchanges: 'Configure Exchanges', + aiScanInterval: 'AI Scan Decision Interval (minutes)', + scanIntervalRecommend: 'Recommended: 3-10 minutes', useTestnet: 'Use Testnet', enabled: 'Enabled', save: 'Save', @@ -296,12 +303,12 @@ export const translations = { scanQRCodeInstructions: 'Scan this QR code with Google Authenticator or Authy', otpSecret: 'Or enter this secret manually:', qrCodeHint: 'QR code (if scanning fails, use the secret below):', - step1Title: 'Step 1: Install Google Authenticator', - step1Desc: 'Download and install Google Authenticator from your app store', - step2Title: 'Step 2: Add account', - step2Desc: 'Tap "+", then choose "Scan QR code" or "Enter a setup key"', - step3Title: 'Step 3: Verify setup', - step3Desc: 'After setup, continue to enter the 6-digit code', + authStep1Title: 'Step 1: Install Google Authenticator', + authStep1Desc: 'Download and install Google Authenticator from your app store', + authStep2Title: 'Step 2: Add account', + authStep2Desc: 'Tap "+", then choose "Scan QR code" or "Enter a setup key"', + authStep3Title: 'Step 3: Verify setup', + authStep3Desc: 'After setup, continue to enter the 6-digit code', setupCompleteContinue: 'I have completed setup, continue', copy: 'Copy', completeRegistration: 'Complete Registration', @@ -317,6 +324,96 @@ export const translations = { passwordRequired: 'Password is required', invalidEmail: 'Invalid email format', passwordTooShort: 'Password must be at least 6 characters', + + // Landing Page + features: 'Features', + howItWorks: 'How it Works', + community: 'Community', + language: 'Language', + loggedInAs: 'Logged in as', + exitLogin: 'Sign Out', + signIn: 'Sign In', + signUp: 'Sign Up', + + // Hero Section + githubStarsInDays: '2.5K+ GitHub Stars in 3 days', + heroTitle1: 'Read the Market.', + heroTitle2: 'Write the Trade.', + heroDescription: 'NOFX is the future standard for AI trading — an open, community-driven agentic trading OS. Supporting Binance, Aster DEX and other exchanges, self-hosted, multi-agent competition, let AI automatically make decisions, execute and optimize trades for you.', + poweredBy: 'Powered by Aster DEX and Binance, strategically invested by Amber.ac.', + + // Landing Page CTA + readyToDefine: 'Ready to define the future of AI trading?', + startWithCrypto: 'Starting with crypto markets, expanding to TradFi. NOFX is the infrastructure of AgentFi.', + getStartedNow: 'Get Started Now', + viewSourceCode: 'View Source Code', + + // Features Section + coreFeatures: 'Core Features', + whyChooseNofx: 'Why Choose NOFX?', + openCommunityDriven: 'Open source, transparent, community-driven AI trading OS', + openSourceSelfHosted: '100% Open Source & Self-Hosted', + openSourceDesc: 'Your framework, your rules. Non-black box, supports custom prompts and multi-models.', + openSourceFeatures1: 'Fully open source code', + openSourceFeatures2: 'Self-hosting deployment support', + openSourceFeatures3: 'Custom AI prompts', + openSourceFeatures4: 'Multi-model support (DeepSeek, Qwen)', + multiAgentCompetition: 'Multi-Agent Intelligent Competition', + multiAgentDesc: 'AI strategies battle at high speed in sandbox, survival of the fittest, achieving strategy evolution.', + multiAgentFeatures1: 'Multiple AI agents running in parallel', + multiAgentFeatures2: 'Automatic strategy optimization', + multiAgentFeatures3: 'Sandbox security testing', + multiAgentFeatures4: 'Cross-market strategy porting', + secureReliableTrading: 'Secure and Reliable Trading', + secureDesc: 'Enterprise-grade security, complete control over your funds and trading strategies.', + secureFeatures1: 'Local private key management', + secureFeatures2: 'Fine-grained API permission control', + secureFeatures3: 'Real-time risk monitoring', + secureFeatures4: 'Trading log auditing', + + // About Section + aboutNofx: 'About NOFX', + whatIsNofx: 'What is NOFX?', + nofxNotAnotherBot: "NOFX is not another trading bot, but the 'Linux' of AI trading —", + nofxDescription1: 'a transparent, trustworthy open source OS that provides a unified', + nofxDescription2: "'decision-risk-execution' layer, supporting all asset classes.", + nofxDescription3: 'Starting with crypto markets (24/7, high volatility perfect testing ground), future expansion to stocks, futures, forex. Core: open architecture, AI', + nofxDescription4: 'Darwinism (multi-agent self-competition, strategy evolution), CodeFi', + nofxDescription5: 'flywheel (developers get point rewards for PR contributions).', + youFullControl: 'You 100% Control', + fullControlDesc: 'Complete control over AI prompts and funds', + startupMessages1: 'Starting automated trading system...', + startupMessages2: 'API server started on port 8080', + startupMessages3: 'Web console http://localhost:3000', + + // How It Works Section + howToStart: 'How to Get Started with NOFX', + fourSimpleSteps: 'Four simple steps to start your AI automated trading journey', + step1Title: 'Clone GitHub Repository', + step1Desc: 'git clone https://github.com/tinkle-community/nofx and switch to dev branch to test new features.', + step2Title: 'Configure Environment', + step2Desc: 'Frontend setup for exchange APIs (like Binance, Hyperliquid), AI models and custom prompts.', + step3Title: 'Deploy & Run', + step3Desc: 'One-click Docker deployment, start AI agents. Note: High-risk market, only test with money you can afford to lose.', + step4Title: 'Optimize & Contribute', + step4Desc: 'Monitor trading, submit PRs to improve framework. Join Telegram to share strategies.', + importantRiskWarning: 'Important Risk Warning', + riskWarningText: 'Dev branch is unstable, do not use funds you cannot afford to lose. NOFX is non-custodial, no official strategies. Trading involves risks, invest carefully.', + + // Community Section (testimonials are kept as-is since they are quotes) + + // Footer Section + futureStandardAI: 'The future standard of AI trading', + links: 'Links', + resources: 'Resources', + documentation: 'Documentation', + supporters: 'Supporters', + strategicInvestment: '(Strategic Investment)', + + // Login Modal + accessNofxPlatform: 'Access NOFX Platform', + loginRegisterPrompt: 'Please login or register to access the full AI trading platform', + registerNewAccount: 'Register New Account', }, zh: { // Header @@ -332,6 +429,11 @@ export const translations = { logout: '退出', switchTrader: '切换交易员:', view: '查看', + + // Navigation + realtimeNav: '实时', + configNav: '配置', + dashboardNav: '看板', // Footer footerTitle: 'NOFX - AI交易系统', @@ -475,6 +577,8 @@ export const translations = { create: '创建', configureAIModels: '配置AI模型', configureExchanges: '配置交易所', + aiScanInterval: 'AI 扫描决策间隔 (分钟)', + scanIntervalRecommend: '建议: 3-10分钟', useTestnet: '使用测试网', enabled: '启用', save: '保存', @@ -613,12 +717,12 @@ export const translations = { scanQRCodeInstructions: '使用Google Authenticator或Authy扫描此二维码', otpSecret: '或手动输入此密钥:', qrCodeHint: '二维码(如果无法扫描,请使用下方密钥):', - step1Title: '步骤1:下载Google Authenticator', - step1Desc: '在手机应用商店下载并安装Google Authenticator应用', - step2Title: '步骤2:添加账户', - step2Desc: '在应用中点击“+”,选择“扫描二维码”或“手动输入密钥”', - step3Title: '步骤3:验证设置', - step3Desc: '设置完成后,点击下方按钮输入6位验证码', + authStep1Title: '步骤1:下载Google Authenticator', + authStep1Desc: '在手机应用商店下载并安装Google Authenticator应用', + authStep2Title: '步骤2:添加账户', + authStep2Desc: '在应用中点击“+”,选择“扫描二维码”或“手动输入密钥”', + authStep3Title: '步骤3:验证设置', + authStep3Desc: '设置完成后,点击下方按钮输入6位验证码', setupCompleteContinue: '我已完成设置,继续', copy: '复制', completeRegistration: '完成注册', @@ -634,6 +738,96 @@ export const translations = { passwordRequired: '请输入密码', invalidEmail: '邮箱格式不正确', passwordTooShort: '密码至少需要6个字符', + + // Landing Page + features: '功能', + howItWorks: '如何运作', + community: '社区', + language: '语言', + loggedInAs: '已登录为', + exitLogin: '退出登录', + signIn: '登录', + signUp: '注册', + + // Hero Section + githubStarsInDays: '3 天内 2.5K+ GitHub Stars', + heroTitle1: 'Read the Market.', + heroTitle2: 'Write the Trade.', + heroDescription: 'NOFX 是 AI 交易的未来标准——一个开放、社区驱动的代理式交易操作系统。支持 Binance、Aster DEX 等交易所,自托管、多代理竞争,让 AI 为你自动决策、执行和优化交易。', + poweredBy: '由 Aster DEX 和 Binance 提供支持,Amber.ac 战略投资。', + + // Landing Page CTA + readyToDefine: '准备好定义 AI 交易的未来吗?', + startWithCrypto: '从加密市场起步,扩展到 TradFi。NOFX 是 AgentFi 的基础架构。', + getStartedNow: '立即开始', + viewSourceCode: '查看源码', + + // Features Section + coreFeatures: '核心功能', + whyChooseNofx: '为什么选择 NOFX?', + openCommunityDriven: '开源、透明、社区驱动的 AI 交易操作系统', + openSourceSelfHosted: '100% 开源与自托管', + openSourceDesc: '你的框架,你的规则。非黑箱,支持自定义提示词和多模型。', + openSourceFeatures1: '完全开源代码', + openSourceFeatures2: '支持自托管部署', + openSourceFeatures3: '自定义 AI 提示词', + openSourceFeatures4: '多模型支持(DeepSeek、Qwen)', + multiAgentCompetition: '多代理智能竞争', + multiAgentDesc: 'AI 策略在沙盒中高速战斗,最优者生存,实现策略进化。', + multiAgentFeatures1: '多 AI 代理并行运行', + multiAgentFeatures2: '策略自动优化', + multiAgentFeatures3: '沙盒安全测试', + multiAgentFeatures4: '跨市场策略移植', + secureReliableTrading: '安全可靠交易', + secureDesc: '企业级安全保障,完全掌控你的资金和交易策略。', + secureFeatures1: '本地私钥管理', + secureFeatures2: 'API 权限精细控制', + secureFeatures3: '实时风险监控', + secureFeatures4: '交易日志审计', + + // About Section + aboutNofx: '关于 NOFX', + whatIsNofx: '什么是 NOFX?', + nofxNotAnotherBot: 'NOFX 不是另一个交易机器人,而是 AI 交易的 \'Linux\' ——', + nofxDescription1: '一个透明、可信任的开源 OS,提供统一的 \'决策-风险-执行\'', + nofxDescription2: '层,支持所有资产类别。', + nofxDescription3: '从加密市场起步(24/7、高波动性完美测试场),未来扩展到股票、期货、外汇。核心:开放架构、AI', + nofxDescription4: '达尔文主义(多代理自竞争、策略进化)、CodeFi 飞轮(开发者 PR', + nofxDescription5: '贡献获积分奖励)。', + youFullControl: '你 100% 掌控', + fullControlDesc: '完全掌控 AI 提示词和资金', + startupMessages1: ' 启动自动交易系统...', + startupMessages2: ' API服务器启动在端口 8080', + startupMessages3: ' Web 控制台 http://localhost:3000', + + // How It Works Section + howToStart: '如何开始使用 NOFX', + fourSimpleSteps: '四个简单步骤,开启 AI 自动交易之旅', + step1Title: '拉取 GitHub 仓库', + step1Desc: 'git clone https://github.com/tinkle-community/nofx 并切换到 dev 分支测试新功能。', + step2Title: '配置环境', + step2Desc: '前端设置交易所 API(如 Binance、Hyperliquid)、AI 模型和自定义提示词。', + step3Title: '部署与运行', + step3Desc: '一键 Docker 部署,启动 AI 代理。注意:高风险市场,仅用闲钱测试。', + step4Title: '优化与贡献', + step4Desc: '监控交易,提交 PR 改进框架。加入 Telegram 分享策略。', + importantRiskWarning: '重要风险提示', + riskWarningText: 'dev 分支不稳定,勿用无法承受损失的资金。NOFX 非托管,无官方策略。交易有风险,投资需谨慎。', + + // Community Section (testimonials are kept as-is since they are quotes) + + // Footer Section + futureStandardAI: 'AI 交易的未来标准', + links: '链接', + resources: '资源', + documentation: '文档', + supporters: '支持方', + strategicInvestment: '(战略投资)', + + // Login Modal + accessNofxPlatform: '访问 NOFX 平台', + loginRegisterPrompt: '请选择登录或注册以访问完整的 AI 交易平台', + registerNewAccount: '注册新账号', } }; diff --git a/web/src/index.css b/web/src/index.css index cc360ff5..46756529 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -5,11 +5,16 @@ @tailwind components; @tailwind utilities; +/* 强制显示滚动条以确保所有页面布局一致 */ +html { + overflow-y: scroll; +} + :root { /* Binance Brand Colors */ --brand-yellow: #F0B90B; - --brand-black: #0C0E12; - --brand-dark-gray: #1E2329; + --brand-black: #000000; + --brand-dark-gray: #0A0A0A; --brand-light-gray: #EAECEF; --brand-almost-white: #FAFAFA; --brand-white: #FFFFFF; @@ -20,14 +25,14 @@ --binance-yellow-light: #FCD535; --binance-yellow-glow: rgba(240, 185, 11, 0.2); - --background: #181A20; /* Binance body bg */ - --header-bg: #0B0E11; /* Binance header bg */ - --background-elevated: #181A20; + --background: #000000; /* Binance body bg */ + --header-bg: #000000; /* Binance header bg */ + --background-elevated: #000000; --foreground: #EAECEF; - --panel-bg: #1E2329; - --panel-bg-hover: #252930; - --panel-border: #2B3139; - --panel-border-hover: #474D57; + --panel-bg: #0A0A0A; + --panel-bg-hover: #111111; + --panel-border: #1A1A1A; + --panel-border-hover: #2A2A2A; /* Binance Signature Colors */ --binance-green: #0ECB81; @@ -44,7 +49,7 @@ --text-disabled: #474D57; /* Chart Colors */ - --grid-stroke: #2B3139; + --grid-stroke: #1A1A1A; --axis-tick: #5E6673; --ref-line: #474D57; diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index c6b0c87c..0e066dce 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -32,13 +32,20 @@ function getAuthHeaders(): Record { export const api = { // AI交易员管理接口 async getTraders(): Promise { - const res = await fetch(`${API_BASE}/traders`, { + const res = await fetch(`${API_BASE}/my-traders`, { headers: getAuthHeaders(), }); if (!res.ok) throw new Error('获取trader列表失败'); return res.json(); }, + // 获取公开的交易员列表(无需认证) + async getPublicTraders(): Promise { + const res = await fetch(`${API_BASE}/traders`); + if (!res.ok) throw new Error('获取公开trader列表失败'); + return res.json(); + }, + async createTrader(request: CreateTraderRequest): Promise { const res = await fetch(`${API_BASE}/traders`, { method: 'POST', @@ -240,6 +247,33 @@ export const api = { return res.json(); }, + // 批量获取多个交易员的历史数据(无需认证) + async getEquityHistoryBatch(traderIds: string[]): Promise { + const res = await fetch(`${API_BASE}/equity-history-batch`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ trader_ids: traderIds }), + }); + if (!res.ok) throw new Error('获取批量历史数据失败'); + return res.json(); + }, + + // 获取前5名交易员数据(无需认证) + async getTopTraders(): Promise { + const res = await fetch(`${API_BASE}/top-traders`); + if (!res.ok) throw new Error('获取前5名交易员失败'); + return res.json(); + }, + + // 获取公开交易员配置(无需认证) + async getPublicTraderConfig(traderId: string): Promise { + const res = await fetch(`${API_BASE}/trader/${traderId}/config`); + if (!res.ok) throw new Error('获取公开交易员配置失败'); + return res.json(); + }, + // 获取AI学习表现分析(支持trader_id) async getPerformance(traderId?: string): Promise { const url = traderId @@ -252,11 +286,9 @@ export const api = { return res.json(); }, - // 获取竞赛数据 + // 获取竞赛数据(无需认证) async getCompetition(): Promise { - const res = await fetch(`${API_BASE}/competition`, { - headers: getAuthHeaders(), - }); + const res = await fetch(`${API_BASE}/competition`); if (!res.ok) throw new Error('获取竞赛数据失败'); return res.json(); }, diff --git a/web/src/lib/config.ts b/web/src/lib/config.ts index d9f2f0ba..de53bf2a 100644 --- a/web/src/lib/config.ts +++ b/web/src/lib/config.ts @@ -1,5 +1,6 @@ export interface SystemConfig { admin_mode: boolean; + beta_mode: boolean; } let configPromise: Promise | null = null; diff --git a/web/src/pages/LandingPage.tsx b/web/src/pages/LandingPage.tsx index 206e2b3b..5f1e9e93 100644 --- a/web/src/pages/LandingPage.tsx +++ b/web/src/pages/LandingPage.tsx @@ -10,43 +10,71 @@ import CommunitySection from '../components/landing/CommunitySection' import AnimatedSection from '../components/landing/AnimatedSection' import LoginModal from '../components/landing/LoginModal' import FooterSection from '../components/landing/FooterSection' +import { useAuth } from '../contexts/AuthContext' +import { useLanguage } from '../contexts/LanguageContext' +import { t } from '../i18n/translations' export function LandingPage() { const [showLoginModal, setShowLoginModal] = useState(false) + const { user, logout } = useAuth() + const { language, setLanguage } = useLanguage() + const isLoggedIn = !!user + + console.log('LandingPage - user:', user, 'isLoggedIn:', isLoggedIn); return ( -
- setShowLoginModal(true)} /> - - - - + <> + setShowLoginModal(true)} + isLoggedIn={isLoggedIn} + isHomePage={true} + language={language} + onLanguageChange={setLanguage} + user={user} + onLogout={logout} + onPageChange={(page) => { + console.log('LandingPage onPageChange called with:', page); + if (page === 'competition') { + window.location.href = '/competition'; + } else if (page === 'traders') { + window.location.href = '/traders'; + } else if (page === 'trader') { + window.location.href = '/dashboard'; + } + }} + /> +
+ + + + {/* CTA */}
- 准备好定义 AI 交易的未来吗? + {t('readyToDefine', language)} - 从加密市场起步,扩展到 TradFi。NOFX 是 AgentFi 的基础架构。 + {t('startWithCrypto', language)}
setShowLoginModal(true)} className='flex items-center gap-2 px-10 py-4 rounded-lg font-semibold text-lg' style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}> - 立即开始 + {t('getStartedNow', language)} - - 查看源码 + + {t('viewSourceCode', language)}
- {showLoginModal && setShowLoginModal(false)} />} - -
+ {showLoginModal && setShowLoginModal(false)} language={language} />} + +
+ ) } diff --git a/web/src/types.ts b/web/src/types.ts index e4ba1199..7d115106 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -125,6 +125,7 @@ export interface CreateTraderRequest { ai_model_id: string; exchange_id: string; initial_balance: number; + scan_interval_minutes?: number; btc_eth_leverage?: number; altcoin_leverage?: number; trading_symbols?: string; @@ -198,5 +199,6 @@ export interface TraderConfigData { use_coin_pool: boolean; use_oi_top: boolean; initial_balance: number; + scan_interval_minutes: number; is_running: boolean; }