fix the main branch history issue from November 3rd.

Merge pull request #395 from NoFxAiOS/beta
fix:fix the main branch history issue from November 3rd.
This commit is contained in:
SkywalkerJi
2025-11-04 13:00:05 +09:00
committed by GitHub
44 changed files with 2645 additions and 807 deletions

View File

@@ -71,29 +71,37 @@ func (s *Server) setupRoutes() {
{ {
// 健康检查 // 健康检查
api.Any("/health", s.handleHealth) api.Any("/health", s.handleHealth)
// 认证相关路由(无需认证) // 认证相关路由(无需认证)
api.POST("/register", s.handleRegister) api.POST("/register", s.handleRegister)
api.POST("/login", s.handleLogin) api.POST("/login", s.handleLogin)
api.POST("/verify-otp", s.handleVerifyOTP) api.POST("/verify-otp", s.handleVerifyOTP)
api.POST("/complete-registration", s.handleCompleteRegistration) api.POST("/complete-registration", s.handleCompleteRegistration)
// 系统支持的模型和交易所(无需认证) // 系统支持的模型和交易所(无需认证)
api.GET("/supported-models", s.handleGetSupportedModels) api.GET("/supported-models", s.handleGetSupportedModels)
api.GET("/supported-exchanges", s.handleGetSupportedExchanges) api.GET("/supported-exchanges", s.handleGetSupportedExchanges)
// 系统配置(无需认证) // 系统配置(无需认证)
api.GET("/config", s.handleGetSystemConfig) api.GET("/config", s.handleGetSystemConfig)
// 系统提示词模板管理(无需认证) // 系统提示词模板管理(无需认证)
api.GET("/prompt-templates", s.handleGetPromptTemplates) api.GET("/prompt-templates", s.handleGetPromptTemplates)
api.GET("/prompt-templates/:name", s.handleGetPromptTemplate) 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()) protected := api.Group("/", s.authMiddleware())
{ {
// AI交易员管理 // AI交易员管理
protected.GET("/traders", s.handleTraderList) protected.GET("/my-traders", s.handleTraderList)
protected.GET("/traders/:id/config", s.handleGetTraderConfig) protected.GET("/traders/:id/config", s.handleGetTraderConfig)
protected.POST("/traders", s.handleCreateTrader) protected.POST("/traders", s.handleCreateTrader)
protected.PUT("/traders/:id", s.handleUpdateTrader) protected.PUT("/traders/:id", s.handleUpdateTrader)
@@ -114,10 +122,6 @@ func (s *Server) setupRoutes() {
protected.GET("/user/signal-sources", s.handleGetUserSignalSource) protected.GET("/user/signal-sources", s.handleGetUserSignalSource)
protected.POST("/user/signal-sources", s.handleSaveUserSignalSource) protected.POST("/user/signal-sources", s.handleSaveUserSignalSource)
// 竞赛总览
protected.GET("/competition", s.handleCompetition)
// 指定trader的数据使用query参数 ?trader_id=xxx // 指定trader的数据使用query参数 ?trader_id=xxx
protected.GET("/status", s.handleStatus) protected.GET("/status", s.handleStatus)
protected.GET("/account", s.handleAccount) protected.GET("/account", s.handleAccount)
@@ -125,7 +129,6 @@ func (s *Server) setupRoutes() {
protected.GET("/decisions", s.handleDecisions) protected.GET("/decisions", s.handleDecisions)
protected.GET("/decisions/latest", s.handleLatestDecisions) protected.GET("/decisions/latest", s.handleLatestDecisions)
protected.GET("/statistics", s.handleStatistics) protected.GET("/statistics", s.handleStatistics)
protected.GET("/equity-history", s.handleEquityHistory)
protected.GET("/performance", s.handlePerformance) 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"} defaultCoins = []string{"BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "HYPEUSDT"}
} }
// 获取杠杆配置 // 获取杠杆配置
btcEthLeverageStr, _ := s.database.GetSystemConfig("btc_eth_leverage") btcEthLeverageStr, _ := s.database.GetSystemConfig("btc_eth_leverage")
altcoinLeverageStr, _ := s.database.GetSystemConfig("altcoin_leverage") altcoinLeverageStr, _ := s.database.GetSystemConfig("altcoin_leverage")
btcEthLeverage := 5 btcEthLeverage := 5
if val, err := strconv.Atoi(btcEthLeverageStr); err == nil && val > 0 { if val, err := strconv.Atoi(btcEthLeverageStr); err == nil && val > 0 {
btcEthLeverage = val btcEthLeverage = val
} }
altcoinLeverage := 5 altcoinLeverage := 5
if val, err := strconv.Atoi(altcoinLeverageStr); err == nil && val > 0 { if val, err := strconv.Atoi(altcoinLeverageStr); err == nil && val > 0 {
altcoinLeverage = val altcoinLeverage = val
} }
// 获取内测模式配置
betaModeStr, _ := s.database.GetSystemConfig("beta_mode")
betaMode := betaModeStr == "true"
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"admin_mode": auth.IsAdminMode(), "admin_mode": auth.IsAdminMode(),
"default_coins": defaultCoins, "beta_mode": betaMode,
"default_coins": defaultCoins,
"btc_eth_leverage": btcEthLeverage, "btc_eth_leverage": btcEthLeverage,
"altcoin_leverage": altcoinLeverage, "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) { func (s *Server) getTraderFromQuery(c *gin.Context) (*manager.TraderManager, string, error) {
userID := c.GetString("user_id") userID := c.GetString("user_id")
traderID := c.Query("trader_id") traderID := c.Query("trader_id")
// 确保用户的交易员已加载到内存中 // 确保用户的交易员已加载到内存中
err := s.traderManager.LoadUserTraders(s.database, userID) err := s.traderManager.LoadUserTraders(s.database, userID)
if err != nil { if err != nil {
log.Printf("⚠️ 加载用户 %s 的交易员失败: %v", userID, err) log.Printf("⚠️ 加载用户 %s 的交易员失败: %v", userID, err)
} }
if traderID == "" { if traderID == "" {
// 如果没有指定trader_id返回该用户的第一个trader // 如果没有指定trader_id返回该用户的第一个trader
ids := s.traderManager.GetTraderIDs() ids := s.traderManager.GetTraderIDs()
if len(ids) == 0 { if len(ids) == 0 {
return nil, "", fmt.Errorf("没有可用的trader") return nil, "", fmt.Errorf("没有可用的trader")
} }
// 获取用户的交易员列表,优先返回用户自己的交易员 // 获取用户的交易员列表,优先返回用户自己的交易员
userTraders, err := s.database.GetTraders(userID) userTraders, err := s.database.GetTraders(userID)
if err == nil && len(userTraders) > 0 { if err == nil && len(userTraders) > 0 {
@@ -200,7 +208,7 @@ func (s *Server) getTraderFromQuery(c *gin.Context) (*manager.TraderManager, str
traderID = ids[0] traderID = ids[0]
} }
} }
return s.traderManager, traderID, nil return s.traderManager, traderID, nil
} }
@@ -210,6 +218,7 @@ type CreateTraderRequest struct {
AIModelID string `json:"ai_model_id" binding:"required"` AIModelID string `json:"ai_model_id" binding:"required"`
ExchangeID string `json:"exchange_id" binding:"required"` ExchangeID string `json:"exchange_id" binding:"required"`
InitialBalance float64 `json:"initial_balance"` InitialBalance float64 `json:"initial_balance"`
ScanIntervalMinutes int `json:"scan_interval_minutes"`
BTCETHLeverage int `json:"btc_eth_leverage"` BTCETHLeverage int `json:"btc_eth_leverage"`
AltcoinLeverage int `json:"altcoin_leverage"` AltcoinLeverage int `json:"altcoin_leverage"`
TradingSymbols string `json:"trading_symbols"` TradingSymbols string `json:"trading_symbols"`
@@ -295,13 +304,13 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
// 生成交易员ID // 生成交易员ID
traderID := fmt.Sprintf("%s_%s_%d", req.ExchangeID, req.AIModelID, time.Now().Unix()) traderID := fmt.Sprintf("%s_%s_%d", req.ExchangeID, req.AIModelID, time.Now().Unix())
// 设置默认值 // 设置默认值
isCrossMargin := true // 默认为全仓模式 isCrossMargin := true // 默认为全仓模式
if req.IsCrossMargin != nil { if req.IsCrossMargin != nil {
isCrossMargin = *req.IsCrossMargin isCrossMargin = *req.IsCrossMargin
} }
// 设置杠杆默认值(从系统配置获取) // 设置杠杆默认值(从系统配置获取)
btcEthLeverage := 5 btcEthLeverage := 5
altcoinLeverage := 5 altcoinLeverage := 5
@@ -325,15 +334,21 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
} }
} }
} }
// 设置系统提示词模板默认值 // 设置系统提示词模板默认值
systemPromptTemplate := "default" systemPromptTemplate := "default"
if req.SystemPromptTemplate != "" { if req.SystemPromptTemplate != "" {
systemPromptTemplate = req.SystemPromptTemplate systemPromptTemplate = req.SystemPromptTemplate
} }
// 创建交易员配置(数据库实体) // 设置扫描间隔默认值
trader := &config.TraderRecord{ scanIntervalMinutes := req.ScanIntervalMinutes
if scanIntervalMinutes <= 0 {
scanIntervalMinutes = 3 // 默认3分钟
}
// 创建交易员配置(数据库实体)
trader := &config.TraderRecord{
ID: traderID, ID: traderID,
UserID: userID, UserID: userID,
Name: req.Name, Name: req.Name,
@@ -349,8 +364,8 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
OverrideBasePrompt: req.OverrideBasePrompt, OverrideBasePrompt: req.OverrideBasePrompt,
SystemPromptTemplate: systemPromptTemplate, SystemPromptTemplate: systemPromptTemplate,
IsCrossMargin: isCrossMargin, IsCrossMargin: isCrossMargin,
ScanIntervalMinutes: 3, // 默认3分钟 ScanIntervalMinutes: scanIntervalMinutes,
IsRunning: false, IsRunning: false,
} }
// 保存到数据库 // 保存到数据库
@@ -379,23 +394,24 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
// UpdateTraderRequest 更新交易员请求 // UpdateTraderRequest 更新交易员请求
type UpdateTraderRequest struct { type UpdateTraderRequest struct {
Name string `json:"name" binding:"required"` Name string `json:"name" binding:"required"`
AIModelID string `json:"ai_model_id" binding:"required"` AIModelID string `json:"ai_model_id" binding:"required"`
ExchangeID string `json:"exchange_id" binding:"required"` ExchangeID string `json:"exchange_id" binding:"required"`
InitialBalance float64 `json:"initial_balance"` InitialBalance float64 `json:"initial_balance"`
BTCETHLeverage int `json:"btc_eth_leverage"` ScanIntervalMinutes int `json:"scan_interval_minutes"`
AltcoinLeverage int `json:"altcoin_leverage"` BTCETHLeverage int `json:"btc_eth_leverage"`
TradingSymbols string `json:"trading_symbols"` AltcoinLeverage int `json:"altcoin_leverage"`
CustomPrompt string `json:"custom_prompt"` TradingSymbols string `json:"trading_symbols"`
OverrideBasePrompt bool `json:"override_base_prompt"` CustomPrompt string `json:"custom_prompt"`
IsCrossMargin *bool `json:"is_cross_margin"` OverrideBasePrompt bool `json:"override_base_prompt"`
IsCrossMargin *bool `json:"is_cross_margin"`
} }
// handleUpdateTrader 更新交易员配置 // handleUpdateTrader 更新交易员配置
func (s *Server) handleUpdateTrader(c *gin.Context) { func (s *Server) handleUpdateTrader(c *gin.Context) {
userID := c.GetString("user_id") userID := c.GetString("user_id")
traderID := c.Param("id") traderID := c.Param("id")
var req UpdateTraderRequest var req UpdateTraderRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 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": "获取交易员列表失败"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "获取交易员列表失败"})
return return
} }
var existingTrader *config.TraderRecord var existingTrader *config.TraderRecord
for _, trader := range traders { for _, trader := range traders {
if trader.ID == traderID { if trader.ID == traderID {
@@ -416,7 +432,7 @@ func (s *Server) handleUpdateTrader(c *gin.Context) {
break break
} }
} }
if existingTrader == nil { if existingTrader == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
return return
@@ -427,7 +443,7 @@ func (s *Server) handleUpdateTrader(c *gin.Context) {
if req.IsCrossMargin != nil { if req.IsCrossMargin != nil {
isCrossMargin = *req.IsCrossMargin isCrossMargin = *req.IsCrossMargin
} }
// 设置杠杆默认值 // 设置杠杆默认值
btcEthLeverage := req.BTCETHLeverage btcEthLeverage := req.BTCETHLeverage
altcoinLeverage := req.AltcoinLeverage altcoinLeverage := req.AltcoinLeverage
@@ -437,23 +453,30 @@ func (s *Server) handleUpdateTrader(c *gin.Context) {
if altcoinLeverage <= 0 { if altcoinLeverage <= 0 {
altcoinLeverage = existingTrader.AltcoinLeverage // 保持原值 altcoinLeverage = existingTrader.AltcoinLeverage // 保持原值
} }
// 更新交易员配置 // 设置扫描间隔,允许更新
trader := &config.TraderRecord{ scanIntervalMinutes := req.ScanIntervalMinutes
ID: traderID, if scanIntervalMinutes <= 0 {
UserID: userID, scanIntervalMinutes = existingTrader.ScanIntervalMinutes // 保持原值
Name: req.Name, }
AIModelID: req.AIModelID,
ExchangeID: req.ExchangeID, // 更新交易员配置
InitialBalance: req.InitialBalance, trader := &config.TraderRecord{
BTCETHLeverage: btcEthLeverage, ID: traderID,
AltcoinLeverage: altcoinLeverage, UserID: userID,
TradingSymbols: req.TradingSymbols, Name: req.Name,
CustomPrompt: req.CustomPrompt, AIModelID: req.AIModelID,
OverrideBasePrompt: req.OverrideBasePrompt, ExchangeID: req.ExchangeID,
IsCrossMargin: isCrossMargin, InitialBalance: req.InitialBalance,
ScanIntervalMinutes: existingTrader.ScanIntervalMinutes, // 保持原值 BTCETHLeverage: btcEthLeverage,
IsRunning: existingTrader.IsRunning, // 保持原值 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) { func (s *Server) handleDeleteTrader(c *gin.Context) {
userID := c.GetString("user_id") userID := c.GetString("user_id")
traderID := c.Param("id") traderID := c.Param("id")
// 从数据库删除 // 从数据库删除
err := s.database.DeleteTrader(userID, traderID) err := s.database.DeleteTrader(userID, traderID)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("删除交易员失败: %v", err)}) c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("删除交易员失败: %v", err)})
return return
} }
// 如果交易员正在运行,先停止它 // 如果交易员正在运行,先停止它
if trader, err := s.traderManager.GetTrader(traderID); err == nil { if trader, err := s.traderManager.GetTrader(traderID); err == nil {
status := trader.GetStatus() status := trader.GetStatus()
@@ -499,28 +522,36 @@ func (s *Server) handleDeleteTrader(c *gin.Context) {
log.Printf("⏹ 已停止运行中的交易员: %s", traderID) log.Printf("⏹ 已停止运行中的交易员: %s", traderID)
} }
} }
log.Printf("✓ 交易员已删除: %s", traderID) log.Printf("✓ 交易员已删除: %s", traderID)
c.JSON(http.StatusOK, gin.H{"message": "交易员已删除"}) c.JSON(http.StatusOK, gin.H{"message": "交易员已删除"})
} }
// handleStartTrader 启动交易员 // handleStartTrader 启动交易员
func (s *Server) handleStartTrader(c *gin.Context) { func (s *Server) handleStartTrader(c *gin.Context) {
userID := c.GetString("user_id")
traderID := c.Param("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) trader, err := s.traderManager.GetTrader(traderID)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
return return
} }
// 检查交易员是否已经在运行 // 检查交易员是否已经在运行
status := trader.GetStatus() status := trader.GetStatus()
if isRunning, ok := status["is_running"].(bool); ok && isRunning { if isRunning, ok := status["is_running"].(bool); ok && isRunning {
c.JSON(http.StatusBadRequest, gin.H{"error": "交易员已在运行中"}) c.JSON(http.StatusBadRequest, gin.H{"error": "交易员已在运行中"})
return return
} }
// 启动交易员 // 启动交易员
go func() { go func() {
log.Printf("▶️ 启动交易员 %s (%s)", traderID, trader.GetName()) 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) log.Printf("❌ 交易员 %s 运行错误: %v", trader.GetName(), err)
} }
}() }()
// 更新数据库中的运行状态 // 更新数据库中的运行状态
userID := c.GetString("user_id")
err = s.database.UpdateTraderStatus(userID, traderID, true) err = s.database.UpdateTraderStatus(userID, traderID, true)
if err != nil { if err != nil {
log.Printf("⚠️ 更新交易员状态失败: %v", err) log.Printf("⚠️ 更新交易员状态失败: %v", err)
} }
log.Printf("✓ 交易员 %s 已启动", trader.GetName()) log.Printf("✓ 交易员 %s 已启动", trader.GetName())
c.JSON(http.StatusOK, gin.H{"message": "交易员已启动"}) c.JSON(http.StatusOK, gin.H{"message": "交易员已启动"})
} }
// handleStopTrader 停止交易员 // handleStopTrader 停止交易员
func (s *Server) handleStopTrader(c *gin.Context) { func (s *Server) handleStopTrader(c *gin.Context) {
userID := c.GetString("user_id")
traderID := c.Param("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) trader, err := s.traderManager.GetTrader(traderID)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
return return
} }
// 检查交易员是否正在运行 // 检查交易员是否正在运行
status := trader.GetStatus() status := trader.GetStatus()
if isRunning, ok := status["is_running"].(bool); ok && !isRunning { if isRunning, ok := status["is_running"].(bool); ok && !isRunning {
c.JSON(http.StatusBadRequest, gin.H{"error": "交易员已停止"}) c.JSON(http.StatusBadRequest, gin.H{"error": "交易员已停止"})
return return
} }
// 停止交易员 // 停止交易员
trader.Stop() trader.Stop()
// 更新数据库中的运行状态 // 更新数据库中的运行状态
userID := c.GetString("user_id")
err = s.database.UpdateTraderStatus(userID, traderID, false) err = s.database.UpdateTraderStatus(userID, traderID, false)
if err != nil { if err != nil {
log.Printf("⚠️ 更新交易员状态失败: %v", err) log.Printf("⚠️ 更新交易员状态失败: %v", err)
} }
log.Printf("⏹ 交易员 %s 已停止", trader.GetName()) log.Printf("⏹ 交易员 %s 已停止", trader.GetName())
c.JSON(http.StatusOK, gin.H{"message": "交易员已停止"}) 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) { func (s *Server) handleUpdateTraderPrompt(c *gin.Context) {
traderID := c.Param("id") traderID := c.Param("id")
userID := c.GetString("user_id") userID := c.GetString("user_id")
var req struct { var req struct {
CustomPrompt string `json:"custom_prompt"` CustomPrompt string `json:"custom_prompt"`
OverrideBasePrompt bool `json:"override_base_prompt"` OverrideBasePrompt bool `json:"override_base_prompt"`
} }
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
// 更新数据库 // 更新数据库
err := s.database.UpdateTraderCustomPrompt(userID, traderID, req.CustomPrompt, req.OverrideBasePrompt) err := s.database.UpdateTraderCustomPrompt(userID, traderID, req.CustomPrompt, req.OverrideBasePrompt)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新自定义prompt失败: %v", err)}) c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新自定义prompt失败: %v", err)})
return return
} }
// 如果trader在内存中更新其custom prompt和override设置 // 如果trader在内存中更新其custom prompt和override设置
trader, err := s.traderManager.GetTrader(traderID) trader, err := s.traderManager.GetTrader(traderID)
if err == nil { if err == nil {
@@ -600,7 +637,7 @@ func (s *Server) handleUpdateTraderPrompt(c *gin.Context) {
trader.SetOverrideBasePrompt(req.OverrideBasePrompt) trader.SetOverrideBasePrompt(req.OverrideBasePrompt)
log.Printf("✓ 已更新交易员 %s 的自定义prompt (覆盖基础=%v)", trader.GetName(), req.OverrideBasePrompt) log.Printf("✓ 已更新交易员 %s 的自定义prompt (覆盖基础=%v)", trader.GetName(), req.OverrideBasePrompt)
} }
c.JSON(http.StatusOK, gin.H{"message": "自定义prompt已更新"}) c.JSON(http.StatusOK, gin.H{"message": "自定义prompt已更新"})
} }
@@ -615,7 +652,7 @@ func (s *Server) handleGetModelConfigs(c *gin.Context) {
return return
} }
log.Printf("✅ 找到 %d 个AI模型配置", len(models)) log.Printf("✅ 找到 %d 个AI模型配置", len(models))
c.JSON(http.StatusOK, models) c.JSON(http.StatusOK, models)
} }
@@ -659,7 +696,7 @@ func (s *Server) handleGetExchangeConfigs(c *gin.Context) {
return return
} }
log.Printf("✅ 找到 %d 个交易所配置", len(exchanges)) log.Printf("✅ 找到 %d 个交易所配置", len(exchanges))
c.JSON(http.StatusOK, exchanges) c.JSON(http.StatusOK, exchanges)
} }
@@ -704,7 +741,7 @@ func (s *Server) handleGetUserSignalSource(c *gin.Context) {
}) })
return return
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"coin_pool_url": source.CoinPoolURL, "coin_pool_url": source.CoinPoolURL,
"oi_top_url": source.OITopURL, "oi_top_url": source.OITopURL,
@@ -718,18 +755,18 @@ func (s *Server) handleSaveUserSignalSource(c *gin.Context) {
CoinPoolURL string `json:"coin_pool_url"` CoinPoolURL string `json:"coin_pool_url"`
OITopURL string `json:"oi_top_url"` OITopURL string `json:"oi_top_url"`
} }
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
err := s.database.CreateUserSignalSource(userID, req.CoinPoolURL, req.OITopURL) err := s.database.CreateUserSignalSource(userID, req.CoinPoolURL, req.OITopURL)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("保存用户信号源配置失败: %v", err)}) c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("保存用户信号源配置失败: %v", err)})
return return
} }
log.Printf("✓ 用户信号源配置已保存: user=%s, coin_pool=%s, oi_top=%s", userID, req.CoinPoolURL, req.OITopURL) log.Printf("✓ 用户信号源配置已保存: user=%s, coin_pool=%s, oi_top=%s", userID, req.CoinPoolURL, req.OITopURL)
c.JSON(http.StatusOK, gin.H{"message": "用户信号源配置已保存"}) c.JSON(http.StatusOK, gin.H{"message": "用户信号源配置已保存"})
} }
@@ -801,30 +838,25 @@ func (s *Server) handleGetTraderConfig(c *gin.Context) {
} }
} }
// AIModelID 应该已经是 provider如 "deepseek"),直接使用 // 返回完整的模型ID不做转换保持与前端模型列表一致
// 如果是旧数据格式(如 "admin_deepseek"),提取 provider 部分
aiModelID := traderConfig.AIModelID aiModelID := traderConfig.AIModelID
// 兼容旧数据:如果包含下划线,提取最后一部分作为 provider
if strings.Contains(aiModelID, "_") {
parts := strings.Split(aiModelID, "_")
aiModelID = parts[len(parts)-1]
}
result := map[string]interface{}{ result := map[string]interface{}{
"trader_id": traderConfig.ID, "trader_id": traderConfig.ID,
"trader_name": traderConfig.Name, "trader_name": traderConfig.Name,
"ai_model": aiModelID, "ai_model": aiModelID,
"exchange_id": traderConfig.ExchangeID, "exchange_id": traderConfig.ExchangeID,
"initial_balance": traderConfig.InitialBalance, "initial_balance": traderConfig.InitialBalance,
"btc_eth_leverage": traderConfig.BTCETHLeverage, "scan_interval_minutes": traderConfig.ScanIntervalMinutes,
"altcoin_leverage": traderConfig.AltcoinLeverage, "btc_eth_leverage": traderConfig.BTCETHLeverage,
"trading_symbols": traderConfig.TradingSymbols, "altcoin_leverage": traderConfig.AltcoinLeverage,
"custom_prompt": traderConfig.CustomPrompt, "trading_symbols": traderConfig.TradingSymbols,
"override_base_prompt": traderConfig.OverrideBasePrompt, "custom_prompt": traderConfig.CustomPrompt,
"is_cross_margin": traderConfig.IsCrossMargin, "override_base_prompt": traderConfig.OverrideBasePrompt,
"use_coin_pool": traderConfig.UseCoinPool, "is_cross_margin": traderConfig.IsCrossMargin,
"use_oi_top": traderConfig.UseOITop, "use_coin_pool": traderConfig.UseCoinPool,
"is_running": isRunning, "use_oi_top": traderConfig.UseOITop,
"is_running": isRunning,
} }
c.JSON(http.StatusOK, result) c.JSON(http.StatusOK, result)
@@ -991,13 +1023,13 @@ func (s *Server) handleStatistics(c *gin.Context) {
// handleCompetition 竞赛总览对比所有trader // handleCompetition 竞赛总览对比所有trader
func (s *Server) handleCompetition(c *gin.Context) { func (s *Server) handleCompetition(c *gin.Context) {
userID := c.GetString("user_id") userID := c.GetString("user_id")
// 确保用户的交易员已加载到内存中 // 确保用户的交易员已加载到内存中
err := s.traderManager.LoadUserTraders(s.database, userID) err := s.traderManager.LoadUserTraders(s.database, userID)
if err != nil { if err != nil {
log.Printf("⚠️ 加载用户 %s 的交易员失败: %v", userID, err) log.Printf("⚠️ 加载用户 %s 的交易员失败: %v", userID, err)
} }
competition, err := s.traderManager.GetCompetitionData() competition, err := s.traderManager.GetCompetitionData()
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
@@ -1005,7 +1037,7 @@ func (s *Server) handleCompetition(c *gin.Context) {
}) })
return return
} }
c.JSON(http.StatusOK, competition) c.JSON(http.StatusOK, competition)
} }
@@ -1132,7 +1164,7 @@ func (s *Server) authMiddleware() gin.HandlerFunc {
c.Next() c.Next()
return return
} }
authHeader := c.GetHeader("Authorization") authHeader := c.GetHeader("Authorization")
if authHeader == "" { if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "缺少Authorization头"}) c.JSON(http.StatusUnauthorized, gin.H{"error": "缺少Authorization头"})
@@ -1168,6 +1200,7 @@ func (s *Server) handleRegister(c *gin.Context) {
var req struct { var req struct {
Email string `json:"email" binding:"required,email"` Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"` Password string `json:"password" binding:"required,min=6"`
BetaCode string `json:"beta_code"`
} }
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
@@ -1175,6 +1208,27 @@ func (s *Server) handleRegister(c *gin.Context) {
return 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) _, err := s.database.GetUserByEmail(req.Email)
if err == nil { if err == nil {
@@ -1212,14 +1266,26 @@ func (s *Server) handleRegister(c *gin.Context) {
return 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设置信息 // 返回OTP设置信息
qrCodeURL := auth.GetOTPQRCodeURL(otpSecret, req.Email) qrCodeURL := auth.GetOTPQRCodeURL(otpSecret, req.Email)
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"user_id": userID, "user_id": userID,
"email": req.Email, "email": req.Email,
"otp_secret": otpSecret, "otp_secret": otpSecret,
"qr_code_url": qrCodeURL, "qr_code_url": qrCodeURL,
"message": "请使用Google Authenticator扫描二维码并验证OTP", "message": "请使用Google Authenticator扫描二维码并验证OTP",
}) })
} }
@@ -1304,8 +1370,8 @@ func (s *Server) handleLogin(c *gin.Context) {
// 检查OTP是否已验证 // 检查OTP是否已验证
if !user.OTPVerified { if !user.OTPVerified {
c.JSON(http.StatusUnauthorized, gin.H{ c.JSON(http.StatusUnauthorized, gin.H{
"error": "账户未完成OTP设置", "error": "账户未完成OTP设置",
"user_id": user.ID, "user_id": user.ID,
"requires_otp_setup": true, "requires_otp_setup": true,
}) })
return return
@@ -1313,9 +1379,9 @@ func (s *Server) handleLogin(c *gin.Context) {
// 返回需要OTP验证的状态 // 返回需要OTP验证的状态
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"user_id": user.ID, "user_id": user.ID,
"email": user.Email, "email": user.Email,
"message": "请输入Google Authenticator验证码", "message": "请输入Google Authenticator验证码",
"requires_otp": true, "requires_otp": true,
}) })
} }
@@ -1377,7 +1443,7 @@ func (s *Server) handleGetSupportedModels(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取支持的AI模型失败"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "获取支持的AI模型失败"})
return return
} }
c.JSON(http.StatusOK, models) c.JSON(http.StatusOK, models)
} }
@@ -1390,7 +1456,7 @@ func (s *Server) handleGetSupportedExchanges(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取支持的交易所失败"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "获取支持的交易所失败"})
return return
} }
c.JSON(http.StatusOK, exchanges) c.JSON(http.StatusOK, exchanges)
} }
@@ -1400,7 +1466,12 @@ func (s *Server) Start() error {
log.Printf("🌐 API服务器启动在 http://localhost%s", addr) log.Printf("🌐 API服务器启动在 http://localhost%s", addr)
log.Printf("📊 API文档:") log.Printf("📊 API文档:")
log.Printf(" • GET /api/health - 健康检查") 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(" • POST /api/traders - 创建新的AI交易员")
log.Printf(" • DELETE /api/traders/:id - 删除AI交易员") log.Printf(" • DELETE /api/traders/:id - 删除AI交易员")
log.Printf(" • POST /api/traders/:id/start - 启动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?trader_id=xxx - 指定trader的决策日志")
log.Printf(" • GET /api/decisions/latest?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/statistics?trader_id=xxx - 指定trader的统计信息")
log.Printf(" • GET /api/equity-history?trader_id=xxx - 指定trader的收益率历史数据")
log.Printf(" • GET /api/performance?trader_id=xxx - 指定trader的AI学习表现分析") log.Printf(" • GET /api/performance?trader_id=xxx - 指定trader的AI学习表现分析")
log.Println() log.Println()
@@ -1426,7 +1496,7 @@ func (s *Server) Start() error {
func (s *Server) handleGetPromptTemplates(c *gin.Context) { func (s *Server) handleGetPromptTemplates(c *gin.Context) {
// 导入 decision 包 // 导入 decision 包
templates := decision.GetAllPromptTemplates() templates := decision.GetAllPromptTemplates()
// 转换为响应格式 // 转换为响应格式
response := make([]map[string]interface{}, 0, len(templates)) response := make([]map[string]interface{}, 0, len(templates))
for _, tmpl := range templates { for _, tmpl := range templates {
@@ -1434,7 +1504,7 @@ func (s *Server) handleGetPromptTemplates(c *gin.Context) {
"name": tmpl.Name, "name": tmpl.Name,
}) })
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"templates": response, "templates": response,
}) })
@@ -1443,15 +1513,224 @@ func (s *Server) handleGetPromptTemplates(c *gin.Context) {
// handleGetPromptTemplate 获取指定名称的提示词模板内容 // handleGetPromptTemplate 获取指定名称的提示词模板内容
func (s *Server) handleGetPromptTemplate(c *gin.Context) { func (s *Server) handleGetPromptTemplate(c *gin.Context) {
templateName := c.Param("name") templateName := c.Param("name")
template, err := decision.GetPromptTemplate(templateName) template, err := decision.GetPromptTemplate(templateName)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("模板不存在: %s", templateName)}) c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("模板不存在: %s", templateName)})
return return
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"name": template.Name, "name": template.Name,
"content": template.Content, "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)
}

View File

@@ -61,7 +61,7 @@ func GenerateOTPSecret() (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
key, err := totp.Generate(totp.GenerateOpts{ key, err := totp.Generate(totp.GenerateOpts{
Issuer: OTPIssuer, Issuer: OTPIssuer,
AccountName: uuid.New().String(), AccountName: uuid.New().String(),
@@ -69,7 +69,7 @@ func GenerateOTPSecret() (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
return key.Secret(), nil return key.Secret(), nil
} }
@@ -118,4 +118,4 @@ func ValidateJWT(tokenString string) (*Claims, error) {
// GetOTPQRCodeURL 获取OTP二维码URL // GetOTPQRCodeURL 获取OTP二维码URL
func GetOTPQRCodeURL(secret, email string) string { func GetOTPQRCodeURL(secret, email string) string {
return fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s", OTPIssuer, email, secret, OTPIssuer) return fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s", OTPIssuer, email, secret, OTPIssuer)
} }

View File

@@ -1,5 +1,6 @@
{ {
"admin_mode": true, "admin_mode": true,
"beta_mode": false,
"leverage": { "leverage": {
"btc_eth_leverage": 5, "btc_eth_leverage": 5,
"altcoin_leverage": 5 "altcoin_leverage": 5

View File

@@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"log" "log"
"nofx/market" "nofx/market"
"os"
"slices" "slices"
"strings" "strings"
"time" "time"
@@ -128,6 +129,15 @@ func (d *Database) createTables() error {
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP 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 // 触发器:自动更新 updated_at
`CREATE TRIGGER IF NOT EXISTS update_users_updated_at `CREATE TRIGGER IF NOT EXISTS update_users_updated_at
AFTER UPDATE ON users 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 trading_symbols TEXT DEFAULT ''`, // 交易币种,逗号分隔
`ALTER TABLE traders ADD COLUMN use_coin_pool BOOLEAN DEFAULT 0`, // 是否使用COIN POOL信号源 `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_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 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_api_url TEXT DEFAULT ''`, // 自定义API地址
`ALTER TABLE ai_models ADD COLUMN custom_model_name TEXT DEFAULT ''`, // 自定义模型名称 `ALTER TABLE ai_models ADD COLUMN custom_model_name TEXT DEFAULT ''`, // 自定义模型名称
@@ -249,16 +258,17 @@ func (d *Database) initDefaultData() error {
// 初始化系统配置 - 创建所有字段设置默认值后续由config.json同步更新 // 初始化系统配置 - 创建所有字段设置默认值后续由config.json同步更新
systemConfigs := map[string]string{ systemConfigs := map[string]string{
"admin_mode": "true", // 默认开启管理员模式,便于首次使用 "admin_mode": "true", // 默认开启管理员模式,便于首次使用
"api_server_port": "8080", // 默认API端口 "beta_mode": "false", // 默认关闭内测模式
"use_default_coins": "true", // 默认使用内置币种列表 "api_server_port": "8080", // 默认API端口
"default_coins": `["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]`, // 默认币种列表JSON格式 "use_default_coins": "true", // 默认使用内置币种列表
"max_daily_loss": "10.0", // 最大日损失百分比 "default_coins": `["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]`, // 默认币种列表JSON格式
"max_drawdown": "20.0", // 最大回撤百分比 "max_daily_loss": "10.0", // 最大日损失百分比
"stop_trading_minutes": "60", // 停止交易时间(分钟) "max_drawdown": "20.0", // 最大回撤百分比
"btc_eth_leverage": "5", // BTC/ETH杠杆倍数 "stop_trading_minutes": "60", // 停止交易时间(分钟)
"altcoin_leverage": "5", // 山寨币杠杆倍数 "btc_eth_leverage": "5", // BTC/ETH杠杆倍数
"jwt_secret": "", // JWT密钥默认为空由config.json或系统生成 "altcoin_leverage": "5", // 山寨币杠杆倍数
"jwt_secret": "", // JWT密钥默认为空由config.json或系统生成
} }
for key, value := range systemConfigs { for key, value := range systemConfigs {
@@ -416,7 +426,6 @@ type TraderRecord struct {
TradingSymbols string `json:"trading_symbols"` // 交易币种,逗号分隔 TradingSymbols string `json:"trading_symbols"` // 交易币种,逗号分隔
UseCoinPool bool `json:"use_coin_pool"` // 是否使用COIN POOL信号源 UseCoinPool bool `json:"use_coin_pool"` // 是否使用COIN POOL信号源
UseOITop bool `json:"use_oi_top"` // 是否使用OI TOP信号源 UseOITop bool `json:"use_oi_top"` // 是否使用OI TOP信号源
UseInsideCoins bool `json:"use_inside_coins"` // 是否使用内置评分信号源
CustomPrompt string `json:"custom_prompt"` // 自定义交易策略prompt CustomPrompt string `json:"custom_prompt"` // 自定义交易策略prompt
OverrideBasePrompt bool `json:"override_base_prompt"` // 是否覆盖基础prompt OverrideBasePrompt bool `json:"override_base_prompt"` // 是否覆盖基础prompt
SystemPromptTemplate string `json:"system_prompt_template"` // 系统提示词模板名称 SystemPromptTemplate string `json:"system_prompt_template"` // 系统提示词模板名称
@@ -772,9 +781,9 @@ func (d *Database) CreateExchange(userID, id, name, typ string, enabled bool, ap
// CreateTrader 创建交易员 // CreateTrader 创建交易员
func (d *Database) CreateTrader(trader *TraderRecord) error { func (d *Database) CreateTrader(trader *TraderRecord) error {
_, err := d.db.Exec(` _, 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) 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,?, ?, ?, ?, ?, ?) 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) `, 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 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, 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(btc_eth_leverage, 5) as btc_eth_leverage, COALESCE(altcoin_leverage, 5) as altcoin_leverage,
COALESCE(trading_symbols, '') as trading_symbols, 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(custom_prompt, '') as custom_prompt, COALESCE(override_base_prompt, 0) as override_base_prompt,
COALESCE(system_prompt_template, 'default') as system_prompt_template, COALESCE(system_prompt_template, 'default') as system_prompt_template,
COALESCE(is_cross_margin, 1) as is_cross_margin, created_at, updated_at 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.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID,
&trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning, &trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning,
&trader.BTCETHLeverage, &trader.AltcoinLeverage, &trader.TradingSymbols, &trader.BTCETHLeverage, &trader.AltcoinLeverage, &trader.TradingSymbols,
&trader.UseCoinPool, &trader.UseOITop, &trader.UseInsideCoins, &trader.UseCoinPool, &trader.UseOITop,
&trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.SystemPromptTemplate, &trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.SystemPromptTemplate,
&trader.IsCrossMargin, &trader.IsCrossMargin,
&trader.CreatedAt, &trader.UpdatedAt, &trader.CreatedAt, &trader.UpdatedAt,
@@ -971,3 +980,105 @@ func (d *Database) GetCustomCoins() []string {
func (d *Database) Close() error { func (d *Database) Close() error {
return d.db.Close() 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
}

View File

@@ -11,6 +11,7 @@ services:
volumes: volumes:
- ./config.json:/app/config.json:ro - ./config.json:/app/config.json:ro
- ./config.db:/app/config.db - ./config.db:/app/config.db
- ./beta_codes.txt:/app/beta_codes.txt:ro
- ./decision_logs:/app/decision_logs - ./decision_logs:/app/decision_logs
- ./prompts:/app/prompts - ./prompts:/app/prompts
- /etc/localtime:/etc/localtime:ro # Sync host time - /etc/localtime:/etc/localtime:ro # Sync host time

221
generate_beta_code.sh Executable file
View File

@@ -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<length; i++)); do
local random_index=$((RANDOM % ${#charset}))
code+="${charset:$random_index:1}"
done
echo "$code"
}
# 读取现有内测码
read_existing_codes() {
local file="$1"
if [ -f "$file" ]; then
grep -v '^$' "$file" 2>/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

6
go.mod
View File

@@ -27,7 +27,6 @@ require (
github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect 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/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // 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-sysinfo v1.15.4 // indirect
github.com/elastic/go-windows v1.0.2 // indirect github.com/elastic/go-windows v1.0.2 // indirect
github.com/ethereum/c-kzg-4844/v2 v2.1.5 // 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/prometheus/procfs v0.17.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // 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/rs/zerolog v1.34.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect
github.com/sonirico/vago v0.9.0 // indirect github.com/sonirico/vago v0.9.0 // indirect
@@ -80,8 +78,4 @@ require (
golang.org/x/tools v0.36.0 // indirect golang.org/x/tools v0.36.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect google.golang.org/protobuf v1.36.9 // indirect
howett.net/plist v1.0.1 // 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
) )

8
go.sum
View File

@@ -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/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 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= 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 h1:A3zQcunCxik14MgXu39cXFXcIw2sFXZ0zL886eyiv1Q=
github.com/elastic/go-sysinfo v1.15.4/go.mod h1:ZBVXmqS368dOn/jvijV/zHLfakWTYHBZPk3G244lHrU= github.com/elastic/go-sysinfo v1.15.4/go.mod h1:ZBVXmqS368dOn/jvijV/zHLfakWTYHBZPk3G244lHrU=
github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI= 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-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 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 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 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= 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/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 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= 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 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 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= 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 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= 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=

60
main.go
View File

@@ -26,12 +26,12 @@ type LeverageConfig struct {
// ConfigFile 配置文件结构,只包含需要同步到数据库的字段 // ConfigFile 配置文件结构,只包含需要同步到数据库的字段
type ConfigFile struct { type ConfigFile struct {
AdminMode bool `json:"admin_mode"` AdminMode bool `json:"admin_mode"`
BetaMode bool `json:"beta_mode"`
APIServerPort int `json:"api_server_port"` APIServerPort int `json:"api_server_port"`
UseDefaultCoins bool `json:"use_default_coins"` UseDefaultCoins bool `json:"use_default_coins"`
DefaultCoins []string `json:"default_coins"` DefaultCoins []string `json:"default_coins"`
CoinPoolAPIURL string `json:"coin_pool_api_url"` CoinPoolAPIURL string `json:"coin_pool_api_url"`
OITopAPIURL string `json:"oi_top_api_url"` OITopAPIURL string `json:"oi_top_api_url"`
InsideCoins bool `json:"inside_coins"`
MaxDailyLoss float64 `json:"max_daily_loss"` MaxDailyLoss float64 `json:"max_daily_loss"`
MaxDrawdown float64 `json:"max_drawdown"` MaxDrawdown float64 `json:"max_drawdown"`
StopTradingMinutes int `json:"stop_trading_minutes"` StopTradingMinutes int `json:"stop_trading_minutes"`
@@ -64,15 +64,15 @@ func syncConfigToDatabase(database *config.Database) error {
// 同步各配置项到数据库 // 同步各配置项到数据库
configs := map[string]string{ configs := map[string]string{
"admin_mode": fmt.Sprintf("%t", configFile.AdminMode), "admin_mode": fmt.Sprintf("%t", configFile.AdminMode),
"api_server_port": strconv.Itoa(configFile.APIServerPort), "beta_mode": fmt.Sprintf("%t", configFile.BetaMode),
"use_default_coins": fmt.Sprintf("%t", configFile.UseDefaultCoins), "api_server_port": strconv.Itoa(configFile.APIServerPort),
"coin_pool_api_url": configFile.CoinPoolAPIURL, "use_default_coins": fmt.Sprintf("%t", configFile.UseDefaultCoins),
"oi_top_api_url": configFile.OITopAPIURL, "coin_pool_api_url": configFile.CoinPoolAPIURL,
"inside_coins": fmt.Sprintf("%t", configFile.InsideCoins), "oi_top_api_url": configFile.OITopAPIURL,
"max_daily_loss": fmt.Sprintf("%.1f", configFile.MaxDailyLoss), "max_daily_loss": fmt.Sprintf("%.1f", configFile.MaxDailyLoss),
"max_drawdown": fmt.Sprintf("%.1f", configFile.MaxDrawdown), "max_drawdown": fmt.Sprintf("%.1f", configFile.MaxDrawdown),
"stop_trading_minutes": strconv.Itoa(configFile.StopTradingMinutes), "stop_trading_minutes": strconv.Itoa(configFile.StopTradingMinutes),
} }
// 同步default_coins转换为JSON字符串存储 // 同步default_coins转换为JSON字符串存储
@@ -109,6 +109,41 @@ func syncConfigToDatabase(database *config.Database) error {
return nil 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() { func main() {
fmt.Println("╔════════════════════════════════════════════════════════════╗") fmt.Println("╔════════════════════════════════════════════════════════════╗")
fmt.Println("║ 🤖 AI多模型交易系统 - 支持 DeepSeek & Qwen ║") fmt.Println("║ 🤖 AI多模型交易系统 - 支持 DeepSeek & Qwen ║")
@@ -133,6 +168,11 @@ func main() {
log.Printf("⚠️ 同步config.json到数据库失败: %v", err) log.Printf("⚠️ 同步config.json到数据库失败: %v", err)
} }
// 加载内测码到数据库
if err := loadBetaCodesToDatabase(database); err != nil {
log.Printf("⚠️ 加载内测码到数据库失败: %v", err)
}
// 获取系统配置 // 获取系统配置
useDefaultCoinsStr, _ := database.GetSystemConfig("use_default_coins") useDefaultCoinsStr, _ := database.GetSystemConfig("use_default_coins")
useDefaultCoins := useDefaultCoinsStr == "true" useDefaultCoins := useDefaultCoinsStr == "true"

View File

@@ -1,27 +1,40 @@
package manager package manager
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
"nofx/config" "nofx/config"
"nofx/trader" "nofx/trader"
"sort"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
) )
// CompetitionCache 竞赛数据缓存
type CompetitionCache struct {
data map[string]interface{}
timestamp time.Time
mu sync.RWMutex
}
// TraderManager 管理多个trader实例 // TraderManager 管理多个trader实例
type TraderManager struct { type TraderManager struct {
traders map[string]*trader.AutoTrader // key: trader ID traders map[string]*trader.AutoTrader // key: trader ID
mu sync.RWMutex competitionCache *CompetitionCache
mu sync.RWMutex
} }
// NewTraderManager 创建trader管理器 // NewTraderManager 创建trader管理器
func NewTraderManager() *TraderManager { func NewTraderManager() *TraderManager {
return &TraderManager{ return &TraderManager{
traders: make(map[string]*trader.AutoTrader), 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模型和交易所配置 // 为每个交易员获取AI模型和交易所配置
for _, traderCfg := range allTraders { for _, traderCfg := range allTraders {
// 获取AI模型配置使用交易员所属的用户ID // 获取AI模型配置使用交易员所属的用户ID
aiModels, err := database.GetAIModels(traderCfg.UserID) aiModels, err := database.GetAIModels(traderCfg.UserID)
if err != nil { if err != nil {
@@ -157,7 +170,7 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro
} }
// 添加到TraderManager // 添加到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 { if err != nil {
log.Printf("❌ 添加交易员 %s 失败: %v", traderCfg.Name, err) log.Printf("❌ 添加交易员 %s 失败: %v", traderCfg.Name, err)
continue continue
@@ -186,7 +199,7 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel
} }
} }
} }
// 如果没有指定交易币种,使用默认币种 // 如果没有指定交易币种,使用默认币种
if len(tradingCoins) == 0 { if len(tradingCoins) == 0 {
tradingCoins = defaultCoins tradingCoins = defaultCoins
@@ -200,7 +213,7 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel
} }
// 构建AutoTraderConfig // 构建AutoTraderConfig
traderConfig := trader.AutoTraderConfig{ traderConfig := trader.AutoTraderConfig{
ID: traderCfg.ID, ID: traderCfg.ID,
Name: traderCfg.Name, Name: traderCfg.Name,
AIModel: aiModelCfg.Provider, // 使用provider作为模型标识 AIModel: aiModelCfg.Provider, // 使用provider作为模型标识
@@ -253,7 +266,7 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel
if err != nil { if err != nil {
return fmt.Errorf("创建trader失败: %w", err) return fmt.Errorf("创建trader失败: %w", err)
} }
// 设置自定义prompt如果有 // 设置自定义prompt如果有
if traderCfg.CustomPrompt != "" { if traderCfg.CustomPrompt != "" {
at.SetCustomPrompt(traderCfg.CustomPrompt) at.SetCustomPrompt(traderCfg.CustomPrompt)
@@ -293,7 +306,7 @@ func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModel
} }
} }
} }
// 如果没有指定交易币种,使用默认币种 // 如果没有指定交易币种,使用默认币种
if len(tradingCoins) == 0 { if len(tradingCoins) == 0 {
tradingCoins = defaultCoins tradingCoins = defaultCoins
@@ -359,7 +372,7 @@ func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModel
if err != nil { if err != nil {
return fmt.Errorf("创建trader失败: %w", err) return fmt.Errorf("创建trader失败: %w", err)
} }
// 设置自定义prompt如果有 // 设置自定义prompt如果有
if traderCfg.CustomPrompt != "" { if traderCfg.CustomPrompt != "" {
at.SetCustomPrompt(traderCfg.CustomPrompt) at.SetCustomPrompt(traderCfg.CustomPrompt)
@@ -478,59 +491,193 @@ func (tm *TraderManager) GetComparisonData() (map[string]interface{}, error) {
// GetCompetitionData 获取竞赛数据(全平台所有交易员) // GetCompetitionData 获取竞赛数据(全平台所有交易员)
func (tm *TraderManager) GetCompetitionData() (map[string]interface{}, error) { func (tm *TraderManager) GetCompetitionData() (map[string]interface{}, error) {
tm.mu.RLock() // 检查缓存是否有效30秒内
defer tm.mu.RUnlock() tm.competitionCache.mu.RLock()
if time.Since(tm.competitionCache.timestamp) < 30*time.Second && len(tm.competitionCache.data) > 0 {
comparison := make(map[string]interface{}) // 返回缓存数据
traders := make([]map[string]interface{}, 0) cachedData := make(map[string]interface{})
for k, v := range tm.competitionCache.data {
// 获取全平台所有交易员 cachedData[k] = v
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"],
}
} }
tm.competitionCache.mu.RUnlock()
traders = append(traders, traderData) 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["traders"] = traders
comparison["count"] = len(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 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是否属于指定用户 // isUserTrader 检查trader是否属于指定用户
func isUserTrader(traderID, userID string) bool { func isUserTrader(traderID, userID string) bool {
// trader ID格式: userID_traderName 或 randomUUID_modelName // trader ID格式: userID_traderName 或 randomUUID_modelName
@@ -708,7 +855,7 @@ func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiMode
} }
} }
} }
// 如果没有指定交易币种,使用默认币种 // 如果没有指定交易币种,使用默认币种
if len(tradingCoins) == 0 { if len(tradingCoins) == 0 {
tradingCoins = defaultCoins tradingCoins = defaultCoins
@@ -723,25 +870,25 @@ func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiMode
// 构建AutoTraderConfig // 构建AutoTraderConfig
traderConfig := trader.AutoTraderConfig{ traderConfig := trader.AutoTraderConfig{
ID: traderCfg.ID, ID: traderCfg.ID,
Name: traderCfg.Name, Name: traderCfg.Name,
AIModel: aiModelCfg.Provider, // 使用provider作为模型标识 AIModel: aiModelCfg.Provider, // 使用provider作为模型标识
Exchange: exchangeCfg.ID, // 使用exchange ID Exchange: exchangeCfg.ID, // 使用exchange ID
InitialBalance: traderCfg.InitialBalance, InitialBalance: traderCfg.InitialBalance,
BTCETHLeverage: traderCfg.BTCETHLeverage, BTCETHLeverage: traderCfg.BTCETHLeverage,
AltcoinLeverage: traderCfg.AltcoinLeverage, AltcoinLeverage: traderCfg.AltcoinLeverage,
ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute, ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute,
CoinPoolAPIURL: effectiveCoinPoolURL, CoinPoolAPIURL: effectiveCoinPoolURL,
CustomAPIURL: aiModelCfg.CustomAPIURL, // 自定义API URL CustomAPIURL: aiModelCfg.CustomAPIURL, // 自定义API URL
CustomModelName: aiModelCfg.CustomModelName, // 自定义模型名称 CustomModelName: aiModelCfg.CustomModelName, // 自定义模型名称
UseQwen: aiModelCfg.Provider == "qwen", UseQwen: aiModelCfg.Provider == "qwen",
MaxDailyLoss: maxDailyLoss, MaxDailyLoss: maxDailyLoss,
MaxDrawdown: maxDrawdown, MaxDrawdown: maxDrawdown,
StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute, StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute,
IsCrossMargin: traderCfg.IsCrossMargin, IsCrossMargin: traderCfg.IsCrossMargin,
DefaultCoins: defaultCoins, DefaultCoins: defaultCoins,
TradingCoins: tradingCoins, TradingCoins: tradingCoins,
SystemPromptTemplate: traderCfg.SystemPromptTemplate, // 系统提示词模板 SystemPromptTemplate: traderCfg.SystemPromptTemplate, // 系统提示词模板
} }
// 根据交易所类型设置API密钥 // 根据交易所类型设置API密钥
@@ -769,7 +916,7 @@ func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiMode
if err != nil { if err != nil {
return fmt.Errorf("创建trader失败: %w", err) return fmt.Errorf("创建trader失败: %w", err)
} }
// 设置自定义prompt如果有 // 设置自定义prompt如果有
if traderCfg.CustomPrompt != "" { if traderCfg.CustomPrompt != "" {
at.SetCustomPrompt(traderCfg.CustomPrompt) at.SetCustomPrompt(traderCfg.CustomPrompt)

View File

@@ -106,8 +106,8 @@ func (m *WSMonitor) initializeHistoricalData() error {
return return
} }
if len(klines4h) > 0 { if len(klines4h) > 0 {
m.klineDataMap4h.Store(s, klines) m.klineDataMap4h.Store(s, klines4h)
log.Printf("已加载 %s 的历史K线数据-4h: %d 条", s, len(klines)) log.Printf("已加载 %s 的历史K线数据-4h: %d 条", s, len(klines4h))
} }
}(symbol) }(symbol)
} }

View File

@@ -27,8 +27,8 @@ import (
// AsterTrader Aster交易平台实现 // AsterTrader Aster交易平台实现
type AsterTrader struct { type AsterTrader struct {
ctx context.Context ctx context.Context
user string // 主钱包地址 (ERC20) user string // 主钱包地址 (ERC20)
signer string // API钱包地址 signer string // API钱包地址
privateKey *ecdsa.PrivateKey // API钱包私钥 privateKey *ecdsa.PrivateKey // API钱包私钥
client *http.Client client *http.Client
baseURL string baseURL string
@@ -99,9 +99,9 @@ func (t *AsterTrader) getPrecision(symbol string) (SymbolPrecision, error) {
body, _ := io.ReadAll(resp.Body) body, _ := io.ReadAll(resp.Body)
var info struct { var info struct {
Symbols []struct { Symbols []struct {
Symbol string `json:"symbol"` Symbol string `json:"symbol"`
PricePrecision int `json:"pricePrecision"` PricePrecision int `json:"pricePrecision"`
QuantityPrecision int `json:"quantityPrecision"` QuantityPrecision int `json:"quantityPrecision"`
Filters []map[string]interface{} `json:"filters"` Filters []map[string]interface{} `json:"filters"`
} `json:"symbols"` } `json:"symbols"`
} }
@@ -506,14 +506,14 @@ func (t *AsterTrader) GetPositions() ([]map[string]interface{}, error) {
// 返回与Binance相同的字段名 // 返回与Binance相同的字段名
result = append(result, map[string]interface{}{ result = append(result, map[string]interface{}{
"symbol": pos["symbol"], "symbol": pos["symbol"],
"side": side, "side": side,
"positionAmt": posAmt, "positionAmt": posAmt,
"entryPrice": entryPrice, "entryPrice": entryPrice,
"markPrice": markPrice, "markPrice": markPrice,
"unRealizedProfit": unRealizedProfit, "unRealizedProfit": unRealizedProfit,
"leverage": leverageVal, "leverage": leverageVal,
"liquidationPrice": liquidationPrice, "liquidationPrice": liquidationPrice,
}) })
} }
@@ -827,18 +827,18 @@ func (t *AsterTrader) SetMarginMode(symbol string, isCrossMargin bool) error {
if !isCrossMargin { if !isCrossMargin {
marginType = "ISOLATED" marginType = "ISOLATED"
} }
params := map[string]interface{}{ params := map[string]interface{}{
"symbol": symbol, "symbol": symbol,
"marginType": marginType, "marginType": marginType,
} }
// 使用request方法调用API // 使用request方法调用API
_, err := t.request("POST", "/fapi/v3/marginType", params) _, err := t.request("POST", "/fapi/v3/marginType", params)
if err != nil { if err != nil {
// 如果错误表示无需更改,忽略错误 // 如果错误表示无需更改,忽略错误
if strings.Contains(err.Error(), "No need to change") || if strings.Contains(err.Error(), "No need to change") ||
strings.Contains(err.Error(), "Margin type cannot be changed") { strings.Contains(err.Error(), "Margin type cannot be changed") {
log.Printf(" ✓ %s 仓位模式已是 %s 或有持仓无法更改", symbol, marginType) log.Printf(" ✓ %s 仓位模式已是 %s 或有持仓无法更改", symbol, marginType)
return nil return nil
} }
@@ -846,7 +846,7 @@ func (t *AsterTrader) SetMarginMode(symbol string, isCrossMargin bool) error {
// 不返回错误,让交易继续 // 不返回错误,让交易继续
return nil return nil
} }
log.Printf(" ✓ %s 仓位模式已设置为 %s", symbol, marginType) log.Printf(" ✓ %s 仓位模式已设置为 %s", symbol, marginType)
return nil return nil
} }

View File

@@ -68,8 +68,8 @@ type AutoTraderConfig struct {
IsCrossMargin bool // true=全仓模式, false=逐仓模式 IsCrossMargin bool // true=全仓模式, false=逐仓模式
// 币种配置 // 币种配置
DefaultCoins []string // 默认币种列表(从数据库获取) DefaultCoins []string // 默认币种列表(从数据库获取)
TradingCoins []string // 实际交易币种列表 TradingCoins []string // 实际交易币种列表
// 系统提示词模板 // 系统提示词模板
SystemPromptTemplate string // 系统提示词模板名称(如 "default", "aggressive" SystemPromptTemplate string // 系统提示词模板名称(如 "default", "aggressive"
@@ -87,9 +87,9 @@ type AutoTrader struct {
decisionLogger *logger.DecisionLogger // 决策日志记录器 decisionLogger *logger.DecisionLogger // 决策日志记录器
initialBalance float64 initialBalance float64
dailyPnL float64 dailyPnL float64
customPrompt string // 自定义交易策略prompt customPrompt string // 自定义交易策略prompt
overrideBasePrompt bool // 是否覆盖基础prompt overrideBasePrompt bool // 是否覆盖基础prompt
systemPromptTemplate string // 系统提示词模板名称 systemPromptTemplate string // 系统提示词模板名称
defaultCoins []string // 默认币种列表(从数据库获取) defaultCoins []string // 默认币种列表(从数据库获取)
tradingCoins []string // 实际交易币种列表 tradingCoins []string // 实际交易币种列表
lastResetTime time.Time lastResetTime time.Time
@@ -1016,7 +1016,7 @@ func (at *AutoTrader) getCandidateCoins() ([]decision.CandidateCoin, error) {
if len(at.tradingCoins) == 0 { if len(at.tradingCoins) == 0 {
// 使用数据库配置的默认币种列表 // 使用数据库配置的默认币种列表
var candidateCoins []decision.CandidateCoin var candidateCoins []decision.CandidateCoin
if len(at.defaultCoins) > 0 { if len(at.defaultCoins) > 0 {
// 使用数据库中配置的默认币种 // 使用数据库中配置的默认币种
for _, coin := range at.defaultCoins { for _, coin := range at.defaultCoins {
@@ -1032,7 +1032,7 @@ func (at *AutoTrader) getCandidateCoins() ([]decision.CandidateCoin, error) {
} else { } else {
// 如果数据库中没有配置默认币种则使用AI500+OI Top作为fallback // 如果数据库中没有配置默认币种则使用AI500+OI Top作为fallback
const ai500Limit = 20 // AI500取前20个评分最高的币种 const ai500Limit = 20 // AI500取前20个评分最高的币种
mergedPool, err := pool.GetMergedCoinPool(ai500Limit) mergedPool, err := pool.GetMergedCoinPool(ai500Limit)
if err != nil { if err != nil {
return nil, fmt.Errorf("获取合并币种池失败: %w", err) return nil, fmt.Errorf("获取合并币种池失败: %w", err)
@@ -1073,11 +1073,11 @@ func (at *AutoTrader) getCandidateCoins() ([]decision.CandidateCoin, error) {
func normalizeSymbol(symbol string) string { func normalizeSymbol(symbol string) string {
// 转为大写 // 转为大写
symbol = strings.ToUpper(strings.TrimSpace(symbol)) symbol = strings.ToUpper(strings.TrimSpace(symbol))
// 确保以USDT结尾 // 确保以USDT结尾
if !strings.HasSuffix(symbol, "USDT") { if !strings.HasSuffix(symbol, "USDT") {
symbol = symbol + "USDT" symbol = symbol + "USDT"
} }
return symbol return symbol
} }

View File

@@ -139,18 +139,18 @@ func (t *FuturesTrader) SetMarginMode(symbol string, isCrossMargin bool) error {
} else { } else {
marginType = futures.MarginTypeIsolated marginType = futures.MarginTypeIsolated
} }
// 尝试设置仓位模式 // 尝试设置仓位模式
err := t.client.NewChangeMarginTypeService(). err := t.client.NewChangeMarginTypeService().
Symbol(symbol). Symbol(symbol).
MarginType(marginType). MarginType(marginType).
Do(context.Background()) Do(context.Background())
marginModeStr := "全仓" marginModeStr := "全仓"
if !isCrossMargin { if !isCrossMargin {
marginModeStr = "逐仓" marginModeStr = "逐仓"
} }
if err != nil { if err != nil {
// 如果错误信息包含"No need to change",说明仓位模式已经是目标值 // 如果错误信息包含"No need to change",说明仓位模式已经是目标值
if contains(err.Error(), "No need to change margin type") { 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 return nil
} }
log.Printf(" ✓ %s 仓位模式已设置为 %s", symbol, marginModeStr) log.Printf(" ✓ %s 仓位模式已设置为 %s", symbol, marginModeStr)
return nil return nil
} }

View File

@@ -17,7 +17,7 @@ type HyperliquidTrader struct {
ctx context.Context ctx context.Context
walletAddr string walletAddr string
meta *hyperliquid.Meta // 缓存meta信息包含精度等 meta *hyperliquid.Meta // 缓存meta信息包含精度等
isCrossMargin bool // 是否为全仓模式 isCrossMargin bool // 是否为全仓模式
} }
// NewHyperliquidTrader 创建Hyperliquid交易器 // NewHyperliquidTrader 创建Hyperliquid交易器

10
web/package-lock.json generated
View File

@@ -72,6 +72,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5", "@babel/generator": "^7.28.5",
@@ -1275,6 +1276,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz",
"integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==",
"devOptional": true, "devOptional": true,
"peer": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^3.0.2" "csstype": "^3.0.2"
@@ -1462,6 +1464,7 @@
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.19", "baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751", "caniuse-lite": "^1.0.30001751",
@@ -2157,6 +2160,7 @@
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true, "dev": true,
"peer": true,
"bin": { "bin": {
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
@@ -2477,6 +2481,7 @@
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -2653,6 +2658,7 @@
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
}, },
@@ -2664,6 +2670,7 @@
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0", "loose-envify": "^1.1.0",
"scheduler": "^0.23.2" "scheduler": "^0.23.2"
@@ -3172,6 +3179,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -3286,6 +3294,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.4", "fdir": "^6.4.4",
@@ -3377,6 +3386,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

BIN
web/public/images/hand.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -7,12 +7,12 @@ import { LoginPage } from './components/LoginPage';
import { RegisterPage } from './components/RegisterPage'; import { RegisterPage } from './components/RegisterPage';
import { CompetitionPage } from './components/CompetitionPage'; import { CompetitionPage } from './components/CompetitionPage';
import { LandingPage } from './pages/LandingPage'; import { LandingPage } from './pages/LandingPage';
import HeaderBar from './components/landing/HeaderBar';
import AILearning from './components/AILearning'; import AILearning from './components/AILearning';
import { LanguageProvider, useLanguage } from './contexts/LanguageContext'; import { LanguageProvider, useLanguage } from './contexts/LanguageContext';
import { AuthProvider, useAuth } from './contexts/AuthContext'; import { AuthProvider, useAuth } from './contexts/AuthContext';
import { t, type Language } from './i18n/translations'; import { t, type Language } from './i18n/translations';
import { useSystemConfig } from './hooks/useSystemConfig'; import { useSystemConfig } from './hooks/useSystemConfig';
import { Zap } from 'lucide-react';
import type { import type {
SystemStatus, SystemStatus,
AccountInfo, AccountInfo,
@@ -44,29 +44,42 @@ function App() {
const { config: systemConfig, loading: configLoading } = useSystemConfig(); const { config: systemConfig, loading: configLoading } = useSystemConfig();
const [route, setRoute] = useState(window.location.pathname); const [route, setRoute] = useState(window.location.pathname);
// 从URL hash读取初始页面状态(支持刷新保持页面) // 从URL路径读取初始页面状态(支持刷新保持页面)
const getInitialPage = (): Page => { const getInitialPage = (): Page => {
const path = window.location.pathname;
const hash = window.location.hash.slice(1); // 去掉 # 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<Page>(getInitialPage()); const [currentPage, setCurrentPage] = useState<Page>(getInitialPage());
const [selectedTraderId, setSelectedTraderId] = useState<string | undefined>(); const [selectedTraderId, setSelectedTraderId] = useState<string | undefined>();
const [lastUpdate, setLastUpdate] = useState<string>('--:--:--'); const [lastUpdate, setLastUpdate] = useState<string>('--:--:--');
// 监听URL hash变化,同步页面状态 // 监听URL变化同步页面状态
useEffect(() => { useEffect(() => {
const handleHashChange = () => { const handleRouteChange = () => {
const path = window.location.pathname;
const hash = window.location.hash.slice(1); 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'); setCurrentPage('trader');
} else if (hash === 'competition' || hash === '') { } else if (path === '/competition' || hash === 'competition' || hash === '') {
setCurrentPage('competition'); setCurrentPage('competition');
} }
setRoute(path);
}; };
window.addEventListener('hashchange', handleHashChange); window.addEventListener('hashchange', handleRouteChange);
return () => window.removeEventListener('hashchange', handleHashChange); window.addEventListener('popstate', handleRouteChange);
return () => {
window.removeEventListener('hashchange', handleRouteChange);
window.removeEventListener('popstate', handleRouteChange);
};
}, []); }, []);
// 切换页面时更新URL hash (当前通过按钮直接调用setCurrentPage这个函数暂时保留用于未来扩展) // 切换页面时更新URL hash (当前通过按钮直接调用setCurrentPage这个函数暂时保留用于未来扩展)
@@ -75,10 +88,14 @@ function App() {
// window.location.hash = page === 'competition' ? '' : 'trader'; // window.location.hash = page === 'competition' ? '' : 'trader';
// }; // };
// 获取trader列表 // 获取trader列表(仅在用户登录时)
const { data: traders } = useSWR<TraderInfo[]>('traders', api.getTraders, { const { data: traders } = useSWR<TraderInfo[]>(
refreshInterval: 10000, user && token ? 'traders' : null,
}); api.getTraders,
{
refreshInterval: 10000,
}
);
// 当获取到traders后设置默认选中第一个 // 当获取到traders后设置默认选中第一个
useEffect(() => { useEffect(() => {
@@ -166,153 +183,129 @@ function App() {
return () => window.removeEventListener('popstate', handlePopState); 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 // Show loading spinner while checking auth or config
if (isLoading || configLoading) { if (isLoading || configLoading) {
return ( return (
<div className="min-h-screen flex items-center justify-center" style={{ background: '#0B0E11' }}> <div className="min-h-screen flex items-center justify-center" style={{ background: '#0B0E11' }}>
<div className="text-center"> <div className="text-center">
<img src="/images/logo.png" alt="NoFx Logo" className="w-16 h-16 mx-auto mb-4 animate-pulse" /> <img src="/icons/nofx.svg" alt="NoFx Logo" className="w-16 h-16 mx-auto mb-4 animate-pulse" />
<p style={{ color: '#EAECEF' }}>{t('loading', language)}</p> <p style={{ color: '#EAECEF' }}>{t('loading', language)}</p>
</div> </div>
</div> </div>
); );
} }
// Show landing page for root route when not authenticated // Handle specific routes regardless of authentication
if (route === '/login') {
return <LoginPage />;
}
if (route === '/register') {
return <RegisterPage />;
}
if (route === '/competition') {
return (
<div className="min-h-screen" style={{ background: '#000000', color: '#EAECEF' }}>
<HeaderBar
isLoggedIn={!!user}
currentPage="competition"
language={language}
onLanguageChange={setLanguage}
user={user}
onLogout={logout}
isAdminMode={systemConfig?.admin_mode}
onPageChange={(page) => {
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);
}}
/>
<main className="max-w-[1920px] mx-auto px-6 py-6 pt-24">
<CompetitionPage />
</main>
</div>
);
}
// Show landing page for root route
if (route === '/' || route === '') {
return <LandingPage />;
}
// Show main app for authenticated users on other routes
if (!systemConfig?.admin_mode && (!user || !token)) { if (!systemConfig?.admin_mode && (!user || !token)) {
if (route === '/login') { // Default to landing page when not authenticated and no specific route
return <LoginPage />;
}
if (route === '/register') {
return <RegisterPage />;
}
// Default to landing page when not authenticated
return <LandingPage />; return <LandingPage />;
} }
return ( return (
<div className="min-h-screen" style={{ background: '#0B0E11', color: '#EAECEF' }}> <div className="min-h-screen" style={{ background: '#0B0E11', color: '#EAECEF' }}>
{/* Header - Binance Style */} <HeaderBar
<header className="glass sticky top-0 z-50 backdrop-blur-xl"> isLoggedIn={!!user}
<div className="max-w-[1920px] mx-auto px-6 py-4"> currentPage={currentPage}
<div className="relative flex items-center"> language={language}
{/* Left - Logo and Title */} onLanguageChange={setLanguage}
<div className="flex items-center gap-3"> user={user}
<div className="w-8 h-8 flex items-center justify-center"> onLogout={logout}
<img src="/icons/nofx.svg?v=2" alt="NOFX" className="w-8 h-8" /> isAdminMode={systemConfig?.admin_mode}
</div> onPageChange={(page) => {
<div> console.log('Main app onPageChange called with:', page);
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
{t('appTitle', language)} if (page === 'competition') {
</h1> window.history.pushState({}, '', '/competition');
<p className="text-xs mono" style={{ color: '#848E9C' }}> setRoute('/competition');
{t('subtitle', language)} setCurrentPage('competition');
</p> } else if (page === 'traders') {
</div> window.history.pushState({}, '', '/traders');
</div> setRoute('/traders');
setCurrentPage('traders');
{/* Center - Page Toggle (absolutely positioned) */} } else if (page === 'trader') {
<div className="absolute left-1/2 transform -translate-x-1/2 flex gap-1 rounded p-1" style={{ background: '#1E2329' }}> window.history.pushState({}, '', '/dashboard');
<button setRoute('/dashboard');
onClick={() => setCurrentPage('competition')} setCurrentPage('trader');
className={`px-3 py-2 rounded text-sm font-semibold transition-all`} }
style={currentPage === 'competition' }}
? { background: '#F0B90B', color: '#000' } />
: { background: 'transparent', color: '#848E9C' }
}
>
{t('aiCompetition', language)}
</button>
<button
onClick={() => setCurrentPage('traders')}
className={`px-3 py-2 rounded text-sm font-semibold transition-all`}
style={currentPage === 'traders'
? { background: '#F0B90B', color: '#000' }
: { background: 'transparent', color: '#848E9C' }
}
>
{t('aiTraders', language)}
</button>
<button
onClick={() => setCurrentPage('trader')}
className={`px-3 py-2 rounded text-sm font-semibold transition-all`}
style={currentPage === 'trader'
? { background: '#F0B90B', color: '#000' }
: { background: 'transparent', color: '#848E9C' }
}
>
{t('tradingPanel', language)}
</button>
</div>
{/* Right - Actions */}
<div className="ml-auto flex items-center gap-3">
{/* User Info - Only show if not in admin mode */}
{!systemConfig?.admin_mode && user && (
<div className="flex items-center gap-2 px-3 py-2 rounded" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
<div className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold" style={{ background: '#F0B90B', color: '#000' }}>
{user.email[0].toUpperCase()}
</div>
<span className="text-sm" style={{ color: '#EAECEF' }}>{user.email}</span>
</div>
)}
{/* Admin Mode Indicator */}
{systemConfig?.admin_mode && (
<div className="flex items-center gap-2 px-3 py-2 rounded" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
<Zap className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="text-sm font-semibold" style={{ color: '#F0B90B' }}>{t('adminMode', language)}</span>
</div>
)}
{/* Language Toggle */}
<div className="flex gap-1 rounded p-1" style={{ background: '#1E2329' }}>
<button
onClick={() => setLanguage('zh')}
className="px-3 py-1.5 rounded text-xs font-semibold transition-all"
style={language === 'zh'
? { background: '#F0B90B', color: '#000' }
: { background: 'transparent', color: '#848E9C' }
}
>
</button>
<button
onClick={() => setLanguage('en')}
className="px-3 py-1.5 rounded text-xs font-semibold transition-all"
style={language === 'en'
? { background: '#F0B90B', color: '#000' }
: { background: 'transparent', color: '#848E9C' }
}
>
EN
</button>
</div>
{/* Logout Button - Only show if not in admin mode */}
{!systemConfig?.admin_mode && (
<button
onClick={logout}
className="px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D', border: '1px solid rgba(246, 70, 93, 0.2)' }}
>
{t('logout', language)}
</button>
)}
</div>
</div>
</div>
</header>
{/* Main Content */} {/* Main Content */}
<main className="max-w-[1920px] mx-auto px-6 py-6"> <main className="max-w-[1920px] mx-auto px-6 py-6 pt-24">
{currentPage === 'competition' ? ( {currentPage === 'competition' ? (
<CompetitionPage /> <CompetitionPage />
) : currentPage === 'traders' ? ( ) : currentPage === 'traders' ? (
<AITradersPage <AITradersPage
onTraderSelect={(traderId) => { onTraderSelect={(traderId) => {
setSelectedTraderId(traderId); setSelectedTraderId(traderId);
window.history.pushState({}, '', '/dashboard');
setRoute('/dashboard');
setCurrentPage('trader'); setCurrentPage('trader');
}} }}
/> />

View File

@@ -4,6 +4,7 @@ import { api } from '../lib/api';
import type { TraderInfo, CreateTraderRequest, AIModel, Exchange } from '../types'; import type { TraderInfo, CreateTraderRequest, AIModel, Exchange } from '../types';
import { useLanguage } from '../contexts/LanguageContext'; import { useLanguage } from '../contexts/LanguageContext';
import { t, type Language } from '../i18n/translations'; import { t, type Language } from '../i18n/translations';
import { useAuth } from '../contexts/AuthContext';
import { getExchangeIcon } from './ExchangeIcons'; import { getExchangeIcon } from './ExchangeIcons';
import { getModelIcon } from './ModelIcons'; import { getModelIcon } from './ModelIcons';
import { TraderConfigModal } from './TraderConfigModal'; import { TraderConfigModal } from './TraderConfigModal';
@@ -35,6 +36,7 @@ interface AITradersPageProps {
export function AITradersPage({ onTraderSelect }: AITradersPageProps) { export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
const { language } = useLanguage(); const { language } = useLanguage();
const { user, token } = useAuth();
const [showCreateModal, setShowCreateModal] = useState(false); const [showCreateModal, setShowCreateModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false);
const [showModelModal, setShowModelModal] = useState(false); const [showModelModal, setShowModelModal] = useState(false);
@@ -53,7 +55,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}); });
const { data: traders, mutate: mutateTraders } = useSWR<TraderInfo[]>( const { data: traders, mutate: mutateTraders } = useSWR<TraderInfo[]>(
'traders', user && token ? 'traders' : null,
api.getTraders, api.getTraders,
{ refreshInterval: 5000 } { refreshInterval: 5000 }
); );
@@ -61,6 +63,21 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
// 加载AI模型和交易所配置 // 加载AI模型和交易所配置
useEffect(() => { useEffect(() => {
const loadConfigs = async () => { 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 { try {
const [modelConfigs, exchangeConfigs, supportedModels, supportedExchanges] = await Promise.all([ const [modelConfigs, exchangeConfigs, supportedModels, supportedExchanges] = await Promise.all([
api.getModelConfigs(), api.getModelConfigs(),
@@ -88,7 +105,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
} }
}; };
loadConfigs(); loadConfigs();
}, []); }, [user, token]);
// 显示所有用户的模型和交易所配置(用于调试) // 显示所有用户的模型和交易所配置(用于调试)
const configuredModels = allModels || []; const configuredModels = allModels || [];
@@ -183,6 +200,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
ai_model_id: data.ai_model_id, ai_model_id: data.ai_model_id,
exchange_id: data.exchange_id, exchange_id: data.exchange_id,
initial_balance: data.initial_balance, initial_balance: data.initial_balance,
scan_interval_minutes: data.scan_interval_minutes,
btc_eth_leverage: data.btc_eth_leverage, btc_eth_leverage: data.btc_eth_leverage,
altcoin_leverage: data.altcoin_leverage, altcoin_leverage: data.altcoin_leverage,
trading_symbols: data.trading_symbols, trading_symbols: data.trading_symbols,
@@ -457,22 +475,22 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}; };
return ( return (
<div className="space-y-6 animate-fade-in"> <div className="space-y-4 md:space-y-6 animate-fade-in">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-3 md:gap-0">
<div className="flex items-center gap-4"> <div className="flex items-center gap-3 md:gap-4">
<div className="w-12 h-12 rounded-xl flex items-center justify-center" style={{ <div className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center" style={{
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)', background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
boxShadow: '0 4px 14px rgba(240, 185, 11, 0.4)' boxShadow: '0 4px 14px rgba(240, 185, 11, 0.4)'
}}> }}>
<Bot className="w-6 h-6" style={{ color: '#000' }} /> <Bot className="w-5 h-5 md:w-6 md:h-6" style={{ color: '#000' }} />
</div> </div>
<div> <div>
<h1 className="text-2xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}> <h1 className="text-xl md:text-2xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
{t('aiTraders', language)} {t('aiTraders', language)}
<span className="text-xs font-normal px-2 py-1 rounded" style={{ <span className="text-xs font-normal px-2 py-1 rounded" style={{
background: 'rgba(240, 185, 11, 0.15)', background: 'rgba(240, 185, 11, 0.15)',
color: '#F0B90B' color: '#F0B90B'
}}> }}>
{traders?.length || 0} {t('active', language)} {traders?.length || 0} {t('active', language)}
</span> </span>
@@ -482,37 +500,37 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
</p> </p>
</div> </div>
</div> </div>
<div className="flex gap-3"> <div className="flex gap-2 md:gap-3 w-full md:w-auto overflow-x-auto flex-wrap md:flex-nowrap">
<button <button
onClick={handleAddModel} onClick={handleAddModel}
className="px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105 flex items-center gap-2" className="px-3 md:px-4 py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 flex items-center gap-1 md:gap-2 whitespace-nowrap"
style={{ style={{
background: '#2B3139', background: '#2B3139',
color: '#EAECEF', color: '#EAECEF',
border: '1px solid #474D57' border: '1px solid #474D57'
}} }}
> >
<Plus className="w-4 h-4" /> <Plus className="w-3 h-3 md:w-4 md:h-4" />
{t('aiModels', language)} {t('aiModels', language)}
</button> </button>
<button <button
onClick={handleAddExchange} onClick={handleAddExchange}
className="px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105 flex items-center gap-2" className="px-3 md:px-4 py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 flex items-center gap-1 md:gap-2 whitespace-nowrap"
style={{ style={{
background: '#2B3139', background: '#2B3139',
color: '#EAECEF', color: '#EAECEF',
border: '1px solid #474D57' border: '1px solid #474D57'
}} }}
> >
<Plus className="w-4 h-4" /> <Plus className="w-3 h-3 md:w-4 md:h-4" />
{t('exchanges', language)} {t('exchanges', language)}
</button> </button>
<button <button
onClick={() => setShowSignalSourceModal(true)} onClick={() => setShowSignalSourceModal(true)}
className="px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105" className="px-3 md:px-4 py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 whitespace-nowrap"
style={{ style={{
background: '#2B3139', background: '#2B3139',
color: '#EAECEF', color: '#EAECEF',
@@ -521,11 +539,11 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
> >
📡 {t('signalSource', language)} 📡 {t('signalSource', language)}
</button> </button>
<button <button
onClick={() => setShowCreateModal(true)} onClick={() => setShowCreateModal(true)}
disabled={configuredModels.length === 0 || configuredExchanges.length === 0} disabled={configuredModels.length === 0 || configuredExchanges.length === 0}
className="px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2" className="px-3 md:px-4 py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1 md:gap-2 whitespace-nowrap"
style={{ style={{
background: (configuredModels.length > 0 && configuredExchanges.length > 0) ? '#F0B90B' : '#2B3139', background: (configuredModels.length > 0 && configuredExchanges.length > 0) ? '#F0B90B' : '#2B3139',
color: (configuredModels.length > 0 && configuredExchanges.length > 0) ? '#000' : '#848E9C' color: (configuredModels.length > 0 && configuredExchanges.length > 0) ? '#000' : '#848E9C'
@@ -538,30 +556,30 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
</div> </div>
{/* Configuration Status */} {/* Configuration Status */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">
{/* AI Models */} {/* AI Models */}
<div className="binance-card p-4"> <div className="binance-card p-3 md:p-4">
<h3 className="text-lg font-semibold mb-3 flex items-center gap-2" style={{ color: '#EAECEF' }}> <h3 className="text-base md:text-lg font-semibold mb-3 flex items-center gap-2" style={{ color: '#EAECEF' }}>
<Brain className="w-5 h-5" style={{ color: '#60a5fa' }} /> <Brain className="w-4 h-4 md:w-5 md:h-5" style={{ color: '#60a5fa' }} />
{t('aiModels', language)} {t('aiModels', language)}
</h3> </h3>
<div className="space-y-3"> <div className="space-y-2 md:space-y-3">
{configuredModels.map(model => { {configuredModels.map(model => {
const inUse = isModelInUse(model.id); const inUse = isModelInUse(model.id);
return ( return (
<div <div
key={model.id} key={model.id}
className={`flex items-center justify-between p-3 rounded transition-all ${ className={`flex items-center justify-between p-2 md:p-3 rounded transition-all ${
inUse ? 'cursor-not-allowed' : 'cursor-pointer hover:bg-gray-700' inUse ? 'cursor-not-allowed' : 'cursor-pointer hover:bg-gray-700'
}`} }`}
style={{ background: '#0B0E11', border: '1px solid #2B3139' }} style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
onClick={() => handleModelClick(model.id)} onClick={() => handleModelClick(model.id)}
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-2 md:gap-3">
<div className="w-8 h-8 flex items-center justify-center"> <div className="w-7 h-7 md:w-8 md:h-8 flex items-center justify-center flex-shrink-0">
{getModelIcon(model.provider || model.id, { width: 32, height: 32 }) || ( {getModelIcon(model.provider || model.id, { width: 28, height: 28 }) || (
<div className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold" <div className="w-7 h-7 md:w-8 md:h-8 rounded-full flex items-center justify-center text-xs md:text-sm font-bold"
style={{ style={{
background: model.id === 'deepseek' ? '#60a5fa' : '#c084fc', background: model.id === 'deepseek' ? '#60a5fa' : '#c084fc',
color: '#fff' color: '#fff'
}}> }}>
@@ -569,63 +587,63 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
</div> </div>
)} )}
</div> </div>
<div> <div className="min-w-0">
<div className="font-semibold" style={{ color: '#EAECEF' }}>{getShortName(model.name)}</div> <div className="font-semibold text-sm md:text-base truncate" style={{ color: '#EAECEF' }}>{getShortName(model.name)}</div>
<div className="text-xs" style={{ color: '#848E9C' }}> <div className="text-xs" style={{ color: '#848E9C' }}>
{inUse ? t('inUse', language) : model.enabled ? t('enabled', language) : t('configured', language)} {inUse ? t('inUse', language) : model.enabled ? t('enabled', language) : t('configured', language)}
</div> </div>
</div> </div>
</div> </div>
<div className={`w-3 h-3 rounded-full ${model.enabled && model.apiKey ? 'bg-green-400' : 'bg-gray-500'}`} /> <div className={`w-2.5 h-2.5 md:w-3 md:h-3 rounded-full flex-shrink-0 ${model.enabled && model.apiKey ? 'bg-green-400' : 'bg-gray-500'}`} />
</div> </div>
); );
})} })}
{configuredModels.length === 0 && ( {configuredModels.length === 0 && (
<div className="text-center py-8" style={{ color: '#848E9C' }}> <div className="text-center py-6 md:py-8" style={{ color: '#848E9C' }}>
<Brain className="w-12 h-12 mx-auto mb-2 opacity-50" /> <Brain className="w-10 h-10 md:w-12 md:h-12 mx-auto mb-2 opacity-50" />
<div className="text-sm">{t('noModelsConfigured', language)}</div> <div className="text-xs md:text-sm">{t('noModelsConfigured', language)}</div>
</div> </div>
)} )}
</div> </div>
</div> </div>
{/* Exchanges */} {/* Exchanges */}
<div className="binance-card p-4"> <div className="binance-card p-3 md:p-4">
<h3 className="text-lg font-semibold mb-3 flex items-center gap-2" style={{ color: '#EAECEF' }}> <h3 className="text-base md:text-lg font-semibold mb-3 flex items-center gap-2" style={{ color: '#EAECEF' }}>
<Landmark className="w-5 h-5" style={{ color: '#F0B90B' }} /> <Landmark className="w-4 h-4 md:w-5 md:h-5" style={{ color: '#F0B90B' }} />
{t('exchanges', language)} {t('exchanges', language)}
</h3> </h3>
<div className="space-y-3"> <div className="space-y-2 md:space-y-3">
{configuredExchanges.map(exchange => { {configuredExchanges.map(exchange => {
const inUse = isExchangeInUse(exchange.id); const inUse = isExchangeInUse(exchange.id);
return ( return (
<div <div
key={exchange.id} key={exchange.id}
className={`flex items-center justify-between p-3 rounded transition-all ${ className={`flex items-center justify-between p-2 md:p-3 rounded transition-all ${
inUse ? 'cursor-not-allowed' : 'cursor-pointer hover:bg-gray-700' inUse ? 'cursor-not-allowed' : 'cursor-pointer hover:bg-gray-700'
}`} }`}
style={{ background: '#0B0E11', border: '1px solid #2B3139' }} style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
onClick={() => handleExchangeClick(exchange.id)} onClick={() => handleExchangeClick(exchange.id)}
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-2 md:gap-3">
<div className="w-8 h-8 flex items-center justify-center"> <div className="w-7 h-7 md:w-8 md:h-8 flex items-center justify-center flex-shrink-0">
{getExchangeIcon(exchange.id, { width: 32, height: 32 })} {getExchangeIcon(exchange.id, { width: 28, height: 28 })}
</div> </div>
<div> <div className="min-w-0">
<div className="font-semibold" style={{ color: '#EAECEF' }}>{getShortName(exchange.name)}</div> <div className="font-semibold text-sm md:text-base truncate" style={{ color: '#EAECEF' }}>{getShortName(exchange.name)}</div>
<div className="text-xs" style={{ color: '#848E9C' }}> <div className="text-xs" style={{ color: '#848E9C' }}>
{exchange.type.toUpperCase()} {inUse ? t('inUse', language) : exchange.enabled ? t('enabled', language) : t('configured', language)} {exchange.type.toUpperCase()} {inUse ? t('inUse', language) : exchange.enabled ? t('enabled', language) : t('configured', language)}
</div> </div>
</div> </div>
</div> </div>
<div className={`w-3 h-3 rounded-full ${exchange.enabled && exchange.apiKey ? 'bg-green-400' : 'bg-gray-500'}`} /> <div className={`w-2.5 h-2.5 md:w-3 md:h-3 rounded-full flex-shrink-0 ${exchange.enabled && exchange.apiKey ? 'bg-green-400' : 'bg-gray-500'}`} />
</div> </div>
); );
})} })}
{configuredExchanges.length === 0 && ( {configuredExchanges.length === 0 && (
<div className="text-center py-8" style={{ color: '#848E9C' }}> <div className="text-center py-6 md:py-8" style={{ color: '#848E9C' }}>
<Landmark className="w-12 h-12 mx-auto mb-2 opacity-50" /> <Landmark className="w-10 h-10 md:w-12 md:h-12 mx-auto mb-2 opacity-50" />
<div className="text-sm">{t('noExchangesConfigured', language)}</div> <div className="text-xs md:text-sm">{t('noExchangesConfigured', language)}</div>
</div> </div>
)} )}
</div> </div>
@@ -633,47 +651,47 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
</div> </div>
{/* Traders List */} {/* Traders List */}
<div className="binance-card p-6"> <div className="binance-card p-4 md:p-6">
<div className="flex items-center justify-between mb-5"> <div className="flex items-center justify-between mb-4 md:mb-5">
<h2 className="text-xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}> <h2 className="text-lg md:text-xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
<Users className="w-6 h-6" style={{ color: '#F0B90B' }} /> <Users className="w-5 h-5 md:w-6 md:h-6" style={{ color: '#F0B90B' }} />
{t('currentTraders', language)} {t('currentTraders', language)}
</h2> </h2>
</div> </div>
{traders && traders.length > 0 ? ( {traders && traders.length > 0 ? (
<div className="space-y-4"> <div className="space-y-3 md:space-y-4">
{traders.map(trader => ( {traders.map(trader => (
<div key={trader.trader_id} <div key={trader.trader_id}
className="flex items-center justify-between p-4 rounded transition-all hover:translate-y-[-1px]" className="flex flex-col md:flex-row md:items-center justify-between p-3 md:p-4 rounded transition-all hover:translate-y-[-1px] gap-3 md:gap-4"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}> style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
<div className="flex items-center gap-4"> <div className="flex items-center gap-3 md:gap-4">
<div className="w-12 h-12 rounded-full flex items-center justify-center" <div className="w-10 h-10 md:w-12 md:h-12 rounded-full flex items-center justify-center flex-shrink-0"
style={{ style={{
background: trader.ai_model.includes('deepseek') ? '#60a5fa' : '#c084fc', background: trader.ai_model.includes('deepseek') ? '#60a5fa' : '#c084fc',
color: '#fff' color: '#fff'
}}> }}>
<Bot className="w-6 h-6" /> <Bot className="w-5 h-5 md:w-6 md:h-6" />
</div> </div>
<div> <div className="min-w-0">
<div className="font-bold text-lg" style={{ color: '#EAECEF' }}> <div className="font-bold text-base md:text-lg truncate" style={{ color: '#EAECEF' }}>
{trader.trader_name} {trader.trader_name}
</div> </div>
<div className="text-sm" style={{ <div className="text-xs md:text-sm truncate" style={{
color: trader.ai_model.includes('deepseek') ? '#60a5fa' : '#c084fc' color: trader.ai_model.includes('deepseek') ? '#60a5fa' : '#c084fc'
}}> }}>
{getModelDisplayName(trader.ai_model.split('_').pop() || trader.ai_model)} Model {trader.exchange_id?.toUpperCase()} {getModelDisplayName(trader.ai_model.split('_').pop() || trader.ai_model)} Model {trader.exchange_id?.toUpperCase()}
</div> </div>
</div> </div>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-3 md:gap-4 flex-wrap md:flex-nowrap">
{/* Status */} {/* Status */}
<div className="text-center"> <div className="text-center">
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>{t('status', language)}</div> <div className="text-xs mb-1" style={{ color: '#848E9C' }}>{t('status', language)}</div>
<div className={`px-3 py-1 rounded text-xs font-bold ${ <div className={`px-2 md:px-3 py-1 rounded text-xs font-bold ${
trader.is_running ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' trader.is_running ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`} style={trader.is_running }`} style={trader.is_running
? { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81' } ? { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81' }
: { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' } : { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }
}> }>
@@ -682,20 +700,20 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
</div> </div>
{/* Actions */} {/* Actions */}
<div className="flex gap-2"> <div className="flex gap-1.5 md:gap-2 flex-wrap md:flex-nowrap">
<button <button
onClick={() => onTraderSelect?.(trader.trader_id)} onClick={() => onTraderSelect?.(trader.trader_id)}
className="px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105 flex items-center gap-1" className="px-2 md:px-3 py-1.5 md:py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 flex items-center gap-1 whitespace-nowrap"
style={{ background: 'rgba(99, 102, 241, 0.1)', color: '#6366F1' }} style={{ background: 'rgba(99, 102, 241, 0.1)', color: '#6366F1' }}
> >
<BarChart3 className="w-4 h-4" /> <BarChart3 className="w-3 h-3 md:w-4 md:h-4" />
{t('view', language)} {t('view', language)}
</button> </button>
<button <button
onClick={() => handleEditTrader(trader.trader_id)} onClick={() => handleEditTrader(trader.trader_id)}
disabled={trader.is_running} disabled={trader.is_running}
className="px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed" className="px-2 md:px-3 py-1.5 md:py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
style={{ style={{
background: trader.is_running ? 'rgba(132, 142, 156, 0.1)' : 'rgba(255, 193, 7, 0.1)', background: trader.is_running ? 'rgba(132, 142, 156, 0.1)' : 'rgba(255, 193, 7, 0.1)',
color: trader.is_running ? '#848E9C' : '#FFC107' color: trader.is_running ? '#848E9C' : '#FFC107'
@@ -703,10 +721,10 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
> >
{t('edit', language)} {t('edit', language)}
</button> </button>
<button <button
onClick={() => handleToggleTrader(trader.trader_id, trader.is_running || false)} onClick={() => handleToggleTrader(trader.trader_id, trader.is_running || false)}
className="px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105" className="px-2 md:px-3 py-1.5 md:py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 whitespace-nowrap"
style={trader.is_running style={trader.is_running
? { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' } ? { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }
: { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81' } : { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81' }
@@ -717,10 +735,10 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
<button <button
onClick={() => handleDeleteTrader(trader.trader_id)} onClick={() => handleDeleteTrader(trader.trader_id)}
className="px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105" className="px-2 md:px-3 py-1.5 md:py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105"
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }} style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-3 h-3 md:w-4 md:h-4" />
</button> </button>
</div> </div>
</div> </div>
@@ -728,15 +746,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
))} ))}
</div> </div>
) : ( ) : (
<div className="text-center py-16" style={{ color: '#848E9C' }}> <div className="text-center py-12 md:py-16" style={{ color: '#848E9C' }}>
<Bot className="w-24 h-24 mx-auto mb-4 opacity-50" /> <Bot className="w-16 h-16 md:w-24 md:h-24 mx-auto mb-3 md:mb-4 opacity-50" />
<div className="text-lg font-semibold mb-2">{t('noTraders', language)}</div> <div className="text-base md:text-lg font-semibold mb-2">{t('noTraders', language)}</div>
<div className="text-sm mb-4">{t('createFirstTrader', language)}</div> <div className="text-xs md:text-sm mb-3 md:mb-4">{t('createFirstTrader', language)}</div>
{(configuredModels.length === 0 || configuredExchanges.length === 0) && ( {(configuredModels.length === 0 || configuredExchanges.length === 0) && (
<div className="text-sm text-yellow-500"> <div className="text-xs md:text-sm text-yellow-500">
{configuredModels.length === 0 && configuredExchanges.length === 0 {configuredModels.length === 0 && configuredExchanges.length === 0
? t('configureModelsAndExchangesFirst', language) ? t('configureModelsAndExchangesFirst', language)
: configuredModels.length === 0 : configuredModels.length === 0
? t('configureModelsFirst', language) ? t('configureModelsFirst', language)
: t('configureExchangesFirst', language) : t('configureExchangesFirst', language)
} }

View File

@@ -31,11 +31,14 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
const { data: allTraderHistories, isLoading } = useSWR( const { data: allTraderHistories, isLoading } = useSWR(
traders.length > 0 ? `all-equity-histories-${tradersKey}` : null, traders.length > 0 ? `all-equity-histories-${tradersKey}` : null,
async () => { async () => {
// 并发请求所有trader的历史数据 // 使用批量API一次性获取所有trader的历史数据
const promises = traders.map(trader => const traderIds = traders.map(trader => trader.trader_id);
api.getEquityHistory(trader.trader_id) const batchData = await api.getEquityHistoryBatch(traderIds);
);
return Promise.all(promises); // 转换为原格式,保持与原有代码兼容
return traders.map(trader => {
return batchData.histories[trader.trader_id] || [];
});
}, },
{ {
refreshInterval: 30000, // 30秒刷新对比图表数据更新频率较低 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, { timestampMap.get(ts)!.traders.set(trader.trader_id, {
pnl_pct: point.total_pnl_pct, pnl_pct: pnlPct,
equity: point.total_equity equity: point.total_equity
}); });
}); });
@@ -225,7 +233,23 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
return ( return (
<div> <div>
<div style={{ borderRadius: '8px', overflow: 'hidden' }}> <div style={{ borderRadius: '8px', overflow: 'hidden', position: 'relative' }}>
{/* NOFX Watermark */}
<div
style={{
position: 'absolute',
top: '20px',
right: '20px',
fontSize: '24px',
fontWeight: 'bold',
color: 'rgba(240, 185, 11, 0.15)',
zIndex: 10,
pointerEvents: 'none',
fontFamily: 'monospace'
}}
>
NOFX
</div>
<ResponsiveContainer width="100%" height={520}> <ResponsiveContainer width="100%" height={520}>
<LineChart data={displayData} margin={{ top: 20, right: 30, left: 20, bottom: 40 }}> <LineChart data={displayData} margin={{ top: 20, right: 30, left: 20, bottom: 40 }}>
<defs> <defs>
@@ -313,24 +337,24 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
</div> </div>
{/* Stats */} {/* Stats */}
<div className="mt-6 grid grid-cols-4 gap-4 pt-5" style={{ borderTop: '1px solid #2B3139' }}> <div className="mt-6 grid grid-cols-2 md:grid-cols-4 gap-3 md:gap-4 pt-5" style={{ borderTop: '1px solid #2B3139' }}>
<div className="p-3 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}> <div className="p-2 md:p-3 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('comparisonMode', language)}</div> <div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('comparisonMode', language)}</div>
<div className="text-base font-bold" style={{ color: '#EAECEF' }}>PnL %</div> <div className="text-sm md:text-base font-bold" style={{ color: '#EAECEF' }}>PnL %</div>
</div> </div>
<div className="p-3 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}> <div className="p-2 md:p-3 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('dataPoints', language)}</div> <div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('dataPoints', language)}</div>
<div className="text-base font-bold mono" style={{ color: '#EAECEF' }}>{t('count', language, {count: combinedData.length})}</div> <div className="text-sm md:text-base font-bold mono" style={{ color: '#EAECEF' }}>{t('count', language, {count: combinedData.length})}</div>
</div> </div>
<div className="p-3 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}> <div className="p-2 md:p-3 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('currentGap', language)}</div> <div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('currentGap', language)}</div>
<div className="text-base font-bold mono" style={{ color: currentGap > 1 ? '#F0B90B' : '#EAECEF' }}> <div className="text-sm md:text-base font-bold mono" style={{ color: currentGap > 1 ? '#F0B90B' : '#EAECEF' }}>
{currentGap.toFixed(2)}% {currentGap.toFixed(2)}%
</div> </div>
</div> </div>
<div className="p-3 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}> <div className="p-2 md:p-3 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('displayRange', language)}</div> <div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('displayRange', language)}</div>
<div className="text-base font-bold mono" style={{ color: '#EAECEF' }}> <div className="text-sm md:text-base font-bold mono" style={{ color: '#EAECEF' }}>
{combinedData.length > MAX_DISPLAY_POINTS {combinedData.length > MAX_DISPLAY_POINTS
? `${t('recent', language)} ${MAX_DISPLAY_POINTS}` ? `${t('recent', language)} ${MAX_DISPLAY_POINTS}`
: t('allData', language)} : t('allData', language)}

View File

@@ -31,6 +31,8 @@ export function CompetitionPage() {
setIsModalOpen(true); setIsModalOpen(true);
} catch (error) { } catch (error) {
console.error('Failed to fetch trader config:', error); console.error('Failed to fetch trader config:', error);
// 对于未登录用户,不显示详细配置,这是正常行为
// 竞赛页面主要用于查看排行榜和基本信息
} }
}; };
@@ -39,7 +41,7 @@ export function CompetitionPage() {
setSelectedTrader(null); setSelectedTrader(null);
}; };
if (!competition || !competition.traders) { if (!competition) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="binance-card p-8 animate-pulse"> <div className="binance-card p-8 animate-pulse">
@@ -62,6 +64,47 @@ export function CompetitionPage() {
); );
} }
// 如果有数据返回但没有交易员,显示空状态
if (!competition.traders || competition.traders.length === 0) {
return (
<div className="space-y-5 animate-fade-in">
{/* Competition Header - 精简版 */}
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-3 md:gap-0">
<div className="flex items-center gap-3 md:gap-4">
<div className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center" style={{
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
boxShadow: '0 4px 14px rgba(240, 185, 11, 0.4)'
}}>
<Trophy className="w-6 h-6 md:w-7 md:h-7" style={{ color: '#000' }} />
</div>
<div>
<h1 className="text-xl md:text-2xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
{t('aiCompetition', language)}
<span className="text-xs font-normal px-2 py-1 rounded" style={{ background: 'rgba(240, 185, 11, 0.15)', color: '#F0B90B' }}>
0 {t('traders', language)}
</span>
</h1>
<p className="text-xs" style={{ color: '#848E9C' }}>
{t('liveBattle', language)}
</p>
</div>
</div>
</div>
{/* Empty State */}
<div className="binance-card p-8 text-center">
<Trophy className="w-16 h-16 mx-auto mb-4 opacity-40" style={{ color: '#848E9C' }} />
<h3 className="text-lg font-bold mb-2" style={{ color: '#EAECEF' }}>
{t('noTraders', language)}
</h3>
<p className="text-sm" style={{ color: '#848E9C' }}>
{t('createFirstTrader', language)}
</p>
</div>
</div>
);
}
// 按收益率排序 // 按收益率排序
const sortedTraders = [...competition.traders].sort( const sortedTraders = [...competition.traders].sort(
(a, b) => b.total_pnl_pct - a.total_pnl_pct (a, b) => b.total_pnl_pct - a.total_pnl_pct
@@ -73,13 +116,16 @@ export function CompetitionPage() {
return ( return (
<div className="space-y-5 animate-fade-in"> <div className="space-y-5 animate-fade-in">
{/* Competition Header - 精简版 */} {/* Competition Header - 精简版 */}
<div className="flex items-center justify-between"> <div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-3 md:gap-0">
<div className="flex items-center gap-4"> <div className="flex items-center gap-3 md:gap-4">
<div className="w-12 h-12 rounded-xl flex items-center justify-center" style={{ background: 'rgba(240, 185, 11, 0.15)', border: '1px solid rgba(240,185,11,0.3)' }}> <div className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center" style={{
<Trophy className="w-6 h-6" style={{ color: '#F0B90B' }} /> background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
boxShadow: '0 4px 14px rgba(240, 185, 11, 0.4)'
}}>
<Trophy className="w-6 h-6 md:w-7 md:h-7" style={{ color: '#000' }} />
</div> </div>
<div> <div>
<h1 className="text-2xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}> <h1 className="text-xl md:text-2xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
{t('aiCompetition', language)} {t('aiCompetition', language)}
<span className="text-xs font-normal px-2 py-1 rounded" style={{ background: 'rgba(240, 185, 11, 0.15)', color: '#F0B90B' }}> <span className="text-xs font-normal px-2 py-1 rounded" style={{ background: 'rgba(240, 185, 11, 0.15)', color: '#F0B90B' }}>
{competition.count} {t('traders', language)} {competition.count} {t('traders', language)}
@@ -90,9 +136,9 @@ export function CompetitionPage() {
</p> </p>
</div> </div>
</div> </div>
<div className="text-right"> <div className="text-left md:text-right w-full md:w-auto">
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>{t('leader', language)}</div> <div className="text-xs mb-1" style={{ color: '#848E9C' }}>{t('leader', language)}</div>
<div className="text-lg font-bold" style={{ color: '#F0B90B' }}>{leader?.trader_name}</div> <div className="text-base md:text-lg font-bold" style={{ color: '#F0B90B' }}>{leader?.trader_name}</div>
<div className="text-sm font-semibold" style={{ color: (leader?.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D' }}> <div className="text-sm font-semibold" style={{ color: (leader?.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D' }}>
{(leader?.total_pnl ?? 0) >= 0 ? '+' : ''}{leader?.total_pnl_pct?.toFixed(2) || '0.00'}% {(leader?.total_pnl ?? 0) >= 0 ? '+' : ''}{leader?.total_pnl_pct?.toFixed(2) || '0.00'}%
</div> </div>
@@ -111,7 +157,7 @@ export function CompetitionPage() {
{t('realTimePnL', language)} {t('realTimePnL', language)}
</div> </div>
</div> </div>
<ComparisonChart traders={sortedTraders} /> <ComparisonChart traders={sortedTraders.slice(0, 5)} />
</div> </div>
{/* Right: Leaderboard */} {/* Right: Leaderboard */}
@@ -155,20 +201,20 @@ export function CompetitionPage() {
</div> </div>
{/* Stats */} {/* Stats */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-2 md:gap-3 flex-wrap md:flex-nowrap">
{/* Total Equity */} {/* Total Equity */}
<div className="text-right"> <div className="text-right">
<div className="text-xs" style={{ color: '#848E9C' }}>{t('equity', language)}</div> <div className="text-xs" style={{ color: '#848E9C' }}>{t('equity', language)}</div>
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}> <div className="text-xs md:text-sm font-bold mono" style={{ color: '#EAECEF' }}>
{trader.total_equity?.toFixed(2) || '0.00'} {trader.total_equity?.toFixed(2) || '0.00'}
</div> </div>
</div> </div>
{/* P&L */} {/* P&L */}
<div className="text-right min-w-[90px]"> <div className="text-right min-w-[70px] md:min-w-[90px]">
<div className="text-xs" style={{ color: '#848E9C' }}>{t('pnl', language)}</div> <div className="text-xs" style={{ color: '#848E9C' }}>{t('pnl', language)}</div>
<div <div
className="text-lg font-bold mono" className="text-base md:text-lg font-bold mono"
style={{ color: (trader.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D' }} style={{ color: (trader.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D' }}
> >
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''} {(trader.total_pnl ?? 0) >= 0 ? '+' : ''}
@@ -182,7 +228,7 @@ export function CompetitionPage() {
{/* Positions */} {/* Positions */}
<div className="text-right"> <div className="text-right">
<div className="text-xs" style={{ color: '#848E9C' }}>{t('pos', language)}</div> <div className="text-xs" style={{ color: '#848E9C' }}>{t('pos', language)}</div>
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}> <div className="text-xs md:text-sm font-bold mono" style={{ color: '#EAECEF' }}>
{trader.position_count} {trader.position_count}
</div> </div>
<div className="text-xs" style={{ color: '#848E9C' }}> <div className="text-xs" style={{ color: '#848E9C' }}>
@@ -242,15 +288,12 @@ export function CompetitionPage() {
> >
<div className="text-center"> <div className="text-center">
<div <div
className="text-base font-bold mb-1" className="text-sm md:text-base font-bold mb-2"
style={{ color: getTraderColor(sortedTraders, trader.trader_id) }} style={{ color: getTraderColor(sortedTraders, trader.trader_id) }}
> >
{trader.trader_name} {trader.trader_name}
</div> </div>
<div className="text-xs mono mb-2" style={{ color: '#848E9C' }}> <div className="text-lg md:text-2xl font-bold mono mb-1" style={{ color: (trader.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D' }}>
{trader.ai_model.toUpperCase()} + {trader.exchange.toUpperCase()}
</div>
<div className="text-2xl font-bold mono mb-1" style={{ color: (trader.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D' }}>
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''}{trader.total_pnl_pct?.toFixed(2) || '0.00'}% {(trader.total_pnl ?? 0) >= 0 ? '+' : ''}{trader.total_pnl_pct?.toFixed(2) || '0.00'}%
</div> </div>
{isWinning && gap > 0 && ( {isWinning && gap > 0 && (

View File

@@ -30,8 +30,8 @@ export const CryptoFeatureCard = React.forwardRef<HTMLDivElement, CryptoFeatureC
<div <div
className={cn( className={cn(
"relative h-full overflow-hidden border-2 transition-all duration-300 rounded-xl", "relative h-full overflow-hidden border-2 transition-all duration-300 rounded-xl",
"bg-gradient-to-br from-[#0C0E12] to-[#1E2329]", "bg-gradient-to-br from-[#000000] to-[#0A0A0A]",
"border-[#2B3139] hover:border-[#F0B90B]/50", "border-[#1A1A1A] hover:border-[#F0B90B]/50",
isHovered && "shadow-[0_0_20px_rgba(240,185,11,0.2)]", isHovered && "shadow-[0_0_20px_rgba(240,185,11,0.2)]",
className className
)} )}
@@ -61,11 +61,11 @@ export const CryptoFeatureCard = React.forwardRef<HTMLDivElement, CryptoFeatureC
<div className="relative z-10 p-8 flex flex-col h-full"> <div className="relative z-10 p-8 flex flex-col h-full">
{/* Icon container */} {/* Icon container */}
<motion.div <motion.div
className={cn( className="mb-6 inline-flex items-center justify-center w-16 h-16 rounded-xl"
"mb-6 inline-flex items-center justify-center w-16 h-16 rounded-xl", style={{
"bg-gradient-to-br from-[#F0B90B]/20 to-[#F0B90B]/5", background: 'linear-gradient(135deg, rgba(240, 185, 11, 0.2) 0%, rgba(240, 185, 11, 0.05) 100%)',
"border border-[#F0B90B]/30" border: '1px solid rgba(240, 185, 11, 0.3)'
)} }}
animate={{ animate={{
scale: isHovered ? 1.1 : 1, scale: isHovered ? 1.1 : 1,
boxShadow: isHovered boxShadow: isHovered
@@ -74,14 +74,14 @@ export const CryptoFeatureCard = React.forwardRef<HTMLDivElement, CryptoFeatureC
}} }}
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
> >
<div className="text-[#F0B90B]">{icon}</div> <div style={{ color: 'var(--brand-yellow)' }}>{icon}</div>
</motion.div> </motion.div>
{/* Title */} {/* Title */}
<h3 className="text-2xl font-bold text-[#EAECEF] mb-3">{title}</h3> <h3 className="text-2xl font-bold mb-3" style={{ color: 'var(--brand-light-gray)' }}>{title}</h3>
{/* Description */} {/* Description */}
<p className="text-[#848E9C] mb-6 flex-grow leading-relaxed">{description}</p> <p className="mb-6 flex-grow leading-relaxed" style={{ color: 'var(--text-secondary)' }}>{description}</p>
{/* Features list */} {/* Features list */}
<div className="space-y-3 mb-6"> <div className="space-y-3 mb-6">
@@ -95,11 +95,11 @@ export const CryptoFeatureCard = React.forwardRef<HTMLDivElement, CryptoFeatureC
className="flex items-start gap-3" className="flex items-start gap-3"
> >
<div className="mt-0.5 flex-shrink-0"> <div className="mt-0.5 flex-shrink-0">
<div className="w-5 h-5 rounded-full bg-[#F0B90B]/20 flex items-center justify-center"> <div className="w-5 h-5 rounded-full flex items-center justify-center" style={{ background: 'rgba(240, 185, 11, 0.2)' }}>
<Check className="w-3 h-3 text-[#F0B90B]" /> <Check className="w-3 h-3" style={{ color: 'var(--brand-yellow)' }} />
</div> </div>
</div> </div>
<span className="text-sm text-[#EAECEF]">{feature}</span> <span className="text-sm" style={{ color: 'var(--brand-light-gray)' }}>{feature}</span>
</motion.div> </motion.div>
))} ))}
</div> </div>

View File

@@ -270,7 +270,23 @@ export function EquityChart({ traderId }: EquityChartProps) {
</div> </div>
{/* Chart */} {/* Chart */}
<div className='my-2' style={{ borderRadius: '8px', overflow: 'hidden' }}> <div className='my-2' style={{ borderRadius: '8px', overflow: 'hidden', position: 'relative' }}>
{/* NOFX Watermark */}
<div
style={{
position: 'absolute',
top: '15px',
right: '15px',
fontSize: '20px',
fontWeight: 'bold',
color: 'rgba(240, 185, 11, 0.15)',
zIndex: 10,
pointerEvents: 'none',
fontFamily: 'monospace'
}}
>
NOFX
</div>
<ResponsiveContainer width='100%' height={280}> <ResponsiveContainer width='100%' height={280}>
<LineChart <LineChart
data={chartData} data={chartData}

View File

@@ -15,7 +15,7 @@ export function Header({ simple = false }: HeaderProps) {
{/* Left - Logo and Title */} {/* Left - Logo and Title */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<img src="/images/logo.png" alt="NoFx Logo" className="w-8 h-8" /> <img src="/icons/nofx.svg" alt="NoFx Logo" className="w-8 h-8" />
</div> </div>
<div> <div>
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}> <h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}>

View File

@@ -2,8 +2,7 @@ import React, { useState } from 'react';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { useLanguage } from '../contexts/LanguageContext'; import { useLanguage } from '../contexts/LanguageContext';
import { t } from '../i18n/translations'; import { t } from '../i18n/translations';
import { Header } from './Header'; import HeaderBar from './landing/HeaderBar';
import { ArrowLeft } from 'lucide-react';
export function LoginPage() { export function LoginPage() {
const { language } = useLanguage(); const { language } = useLanguage();
@@ -51,43 +50,44 @@ export function LoginPage() {
}; };
return ( return (
<div className="min-h-screen" style={{ background: '#0B0E11' }}> <div className="min-h-screen" style={{ background: 'var(--brand-black)' }}>
<Header simple /> <HeaderBar
onLoginClick={() => {}}
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';
}
}}
/>
<div className="flex items-center justify-center" style={{ minHeight: 'calc(100vh - 80px)' }}> <div className="flex items-center justify-center pt-20" style={{ minHeight: 'calc(100vh - 80px)' }}>
<div className="w-full max-w-md"> <div className="w-full max-w-md">
{/* Back to Home */}
<button
onClick={() => {
window.history.pushState({}, '', '/');
window.dispatchEvent(new PopStateEvent('popstate'));
}}
className="flex items-center gap-2 mb-6 text-sm hover:text-[#F0B90B] transition-colors"
style={{ color: '#848E9C' }}
>
<ArrowLeft className="w-4 h-4" />
</button>
{/* Logo */} {/* Logo */}
<div className="text-center mb-8"> <div className="text-center mb-8">
<div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center"> <div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center">
<img src="/images/logo.png" alt="NoFx Logo" className="w-16 h-16 object-contain" /> <img src="/icons/nofx.svg" alt="NoFx Logo" className="w-16 h-16 object-contain" />
</div> </div>
<h1 className="text-2xl font-bold" style={{ color: '#EAECEF' }}> <h1 className="text-2xl font-bold" style={{ color: 'var(--brand-light-gray)' }}>
{t('loginTitle', language)} NOFX
</h1> </h1>
<p className="text-sm mt-2" style={{ color: '#848E9C' }}> <p className="text-sm mt-2" style={{ color: 'var(--text-secondary)' }}>
{step === 'login' ? t('loginTitle', language) : t('enterOTPCode', language)} {step === 'login' ? '请输入您的邮箱和密码' : '请输入两步验证码'}
</p> </p>
</div> </div>
{/* Login Form */} {/* Login Form */}
<div className="rounded-lg p-6" style={{ background: '#1E2329', border: '1px solid #2B3139' }}> <div className="rounded-lg p-6" style={{ background: 'var(--panel-bg)', border: '1px solid var(--panel-border)' }}>
{step === 'login' ? ( {step === 'login' ? (
<form onSubmit={handleLogin} className="space-y-4"> <form onSubmit={handleLogin} className="space-y-4">
<div> <div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}> <label className="block text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
{t('email', language)} {t('email', language)}
</label> </label>
<input <input
@@ -95,14 +95,14 @@ export function LoginPage() {
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
className="w-full px-3 py-2 rounded" className="w-full px-3 py-2 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }}
placeholder={t('emailPlaceholder', language)} placeholder={t('emailPlaceholder', language)}
required required
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}> <label className="block text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
{t('password', language)} {t('password', language)}
</label> </label>
<input <input
@@ -110,14 +110,14 @@ export function LoginPage() {
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
className="w-full px-3 py-2 rounded" className="w-full px-3 py-2 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }}
placeholder={t('passwordPlaceholder', language)} placeholder={t('passwordPlaceholder', language)}
required required
/> />
</div> </div>
{error && ( {error && (
<div className="text-sm px-3 py-2 rounded" style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}> <div className="text-sm px-3 py-2 rounded" style={{ background: 'var(--binance-red-bg)', color: 'var(--binance-red)' }}>
{error} {error}
</div> </div>
)} )}
@@ -126,7 +126,7 @@ export function LoginPage() {
type="submit" type="submit"
disabled={loading} disabled={loading}
className="w-full px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50" 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)} {loading ? t('loading', language) : t('loginButton', language)}
</button> </button>
@@ -142,7 +142,7 @@ export function LoginPage() {
</div> </div>
<div> <div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}> <label className="block text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
{t('otpCode', language)} {t('otpCode', language)}
</label> </label>
<input <input
@@ -150,7 +150,7 @@ export function LoginPage() {
value={otpCode} value={otpCode}
onChange={(e) => setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6))} onChange={(e) => setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
className="w-full px-3 py-2 rounded text-center text-2xl font-mono" className="w-full px-3 py-2 rounded text-center text-2xl font-mono"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }}
placeholder={t('otpPlaceholder', language)} placeholder={t('otpPlaceholder', language)}
maxLength={6} maxLength={6}
required required
@@ -158,7 +158,7 @@ export function LoginPage() {
</div> </div>
{error && ( {error && (
<div className="text-sm px-3 py-2 rounded" style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}> <div className="text-sm px-3 py-2 rounded" style={{ background: 'var(--binance-red-bg)', color: 'var(--binance-red)' }}>
{error} {error}
</div> </div>
)} )}
@@ -168,7 +168,7 @@ export function LoginPage() {
type="button" type="button"
onClick={() => setStep('login')} onClick={() => setStep('login')}
className="flex-1 px-4 py-2 rounded text-sm font-semibold" 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)} {t('back', language)}
</button> </button>
@@ -187,17 +187,17 @@ export function LoginPage() {
{/* Register Link */} {/* Register Link */}
<div className="text-center mt-6"> <div className="text-center mt-6">
<p className="text-sm" style={{ color: '#848E9C' }}> <p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
{t('noAccount', language)}{' '} {' '}
<button <button
onClick={() => { onClick={() => {
window.history.pushState({}, '', '/register'); window.history.pushState({}, '', '/register');
window.dispatchEvent(new PopStateEvent('popstate')); window.dispatchEvent(new PopStateEvent('popstate'));
}} }}
className="font-semibold hover:underline" className="font-semibold hover:underline transition-colors"
style={{ color: '#F0B90B' }} style={{ color: 'var(--brand-yellow)' }}
> >
{t('registerNow', language)}
</button> </button>
</p> </p>
</div> </div>

View File

@@ -1,8 +1,9 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { useLanguage } from '../contexts/LanguageContext'; import { useLanguage } from '../contexts/LanguageContext';
import { t } from '../i18n/translations'; import { t } from '../i18n/translations';
import { ArrowLeft } from 'lucide-react'; import { getSystemConfig } from '../lib/config';
import HeaderBar from './landing/HeaderBar';
export function RegisterPage() { export function RegisterPage() {
const { language } = useLanguage(); const { language } = useLanguage();
@@ -11,6 +12,8 @@ export function RegisterPage() {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState('');
const [betaCode, setBetaCode] = useState('');
const [betaMode, setBetaMode] = useState(false);
const [otpCode, setOtpCode] = useState(''); const [otpCode, setOtpCode] = useState('');
const [userID, setUserID] = useState(''); const [userID, setUserID] = useState('');
const [otpSecret, setOtpSecret] = useState(''); const [otpSecret, setOtpSecret] = useState('');
@@ -18,6 +21,15 @@ export function RegisterPage() {
const [error, setError] = useState(''); const [error, setError] = useState('');
const [loading, setLoading] = useState(false); 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) => { const handleRegister = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setError(''); setError('');
@@ -32,9 +44,14 @@ export function RegisterPage() {
return; return;
} }
if (betaMode && !betaCode.trim()) {
setError('内测期间,注册需要提供内测码');
return;
}
setLoading(true); setLoading(true);
const result = await register(email, password); const result = await register(email, password, betaCode.trim() || undefined);
if (result.success && result.userID) { if (result.success && result.userID) {
setUserID(result.userID); setUserID(result.userID);
@@ -72,27 +89,28 @@ export function RegisterPage() {
}; };
return ( return (
<div className="min-h-screen flex items-center justify-center" style={{ background: '#0B0E11' }}> <div className="min-h-screen" style={{ background: 'var(--brand-black)' }}>
<div className="w-full max-w-md"> <HeaderBar
{/* Back to Home */} isLoggedIn={false}
{step === 'register' && ( isHomePage={false}
<button currentPage="register"
onClick={() => { language={language}
window.history.pushState({}, '', '/'); onLanguageChange={() => {}}
window.dispatchEvent(new PopStateEvent('popstate')); onPageChange={(page) => {
}} console.log('RegisterPage onPageChange called with:', page);
className="flex items-center gap-2 mb-6 text-sm hover:text-[#F0B90B] transition-colors" if (page === 'competition') {
style={{ color: '#848E9C' }} window.location.href = '/competition';
> }
<ArrowLeft className="w-4 h-4" /> }}
/>
</button>
)}
{/* Logo */} <div className="flex items-center justify-center pt-20" style={{ minHeight: 'calc(100vh - 80px)' }}>
<div className="text-center mb-8"> <div className="w-full max-w-md">
{/* Logo */}
<div className="text-center mb-8">
<div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center"> <div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center">
<img src="/images/logo.png" alt="NoFx Logo" className="w-16 h-16 object-contain" /> <img src="/icons/nofx.svg" alt="NoFx Logo" className="w-16 h-16 object-contain" />
</div> </div>
<h1 className="text-2xl font-bold" style={{ color: '#EAECEF' }}> <h1 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>
{t('appTitle', language)} {t('appTitle', language)}
@@ -105,11 +123,11 @@ export function RegisterPage() {
</div> </div>
{/* Registration Form */} {/* Registration Form */}
<div className="rounded-lg p-6" style={{ background: '#1E2329', border: '1px solid #2B3139' }}> <div className="rounded-lg p-6" style={{ background: 'var(--panel-bg)', border: '1px solid var(--panel-border)' }}>
{step === 'register' && ( {step === 'register' && (
<form onSubmit={handleRegister} className="space-y-4"> <form onSubmit={handleRegister} className="space-y-4">
<div> <div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}> <label className="block text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
{t('email', language)} {t('email', language)}
</label> </label>
<input <input
@@ -117,14 +135,14 @@ export function RegisterPage() {
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
className="w-full px-3 py-2 rounded" className="w-full px-3 py-2 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }}
placeholder={t('emailPlaceholder', language)} placeholder={t('emailPlaceholder', language)}
required required
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}> <label className="block text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
{t('password', language)} {t('password', language)}
</label> </label>
<input <input
@@ -132,14 +150,14 @@ export function RegisterPage() {
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
className="w-full px-3 py-2 rounded" className="w-full px-3 py-2 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }}
placeholder={t('passwordPlaceholder', language)} placeholder={t('passwordPlaceholder', language)}
required required
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}> <label className="block text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
{t('confirmPassword', language)} {t('confirmPassword', language)}
</label> </label>
<input <input
@@ -147,23 +165,44 @@ export function RegisterPage() {
value={confirmPassword} value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)} onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full px-3 py-2 rounded" className="w-full px-3 py-2 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }}
placeholder={t('confirmPasswordPlaceholder', language)} placeholder={t('confirmPasswordPlaceholder', language)}
required required
/> />
</div> </div>
{betaMode && (
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
*
</label>
<input
type="text"
value={betaCode}
onChange={(e) => 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}
/>
<p className="text-xs mt-1" style={{ color: '#848E9C' }}>
6
</p>
</div>
)}
{error && ( {error && (
<div className="text-sm px-3 py-2 rounded" style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}> <div className="text-sm px-3 py-2 rounded" style={{ background: 'var(--binance-red-bg)', color: 'var(--binance-red)' }}>
{error} {error}
</div> </div>
)} )}
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading || (betaMode && !betaCode.trim())}
className="w-full px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50" 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('registerButton', language)} {loading ? t('loading', language) : t('registerButton', language)}
</button> </button>
@@ -183,21 +222,21 @@ export function RegisterPage() {
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
<div className="p-3 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}> <div className="p-3 rounded" style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)' }}>
<p className="text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}> <p className="text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
{t('step1Title', language)} {t('authStep1Title', language)}
</p> </p>
<p className="text-xs" style={{ color: '#848E9C' }}> <p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
{t('step1Desc', language)} {t('authStep1Desc', language)}
</p> </p>
</div> </div>
<div className="p-3 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}> <div className="p-3 rounded" style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)' }}>
<p className="text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}> <p className="text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
{t('step2Title', language)} {t('authStep2Title', language)}
</p> </p>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}> <p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('step2Desc', language)} {t('authStep2Desc', language)}
</p> </p>
{qrCodeURL && ( {qrCodeURL && (
@@ -214,13 +253,13 @@ export function RegisterPage() {
<p className="text-xs mb-1" style={{ color: '#848E9C' }}>{t('otpSecret', language)}</p> <p className="text-xs mb-1" style={{ color: '#848E9C' }}>{t('otpSecret', language)}</p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<code className="flex-1 px-2 py-1 text-xs rounded font-mono" <code className="flex-1 px-2 py-1 text-xs rounded font-mono"
style={{ background: '#2B3139', color: '#EAECEF' }}> style={{ background: 'var(--panel-bg-hover)', color: 'var(--brand-light-gray)' }}>
{otpSecret} {otpSecret}
</code> </code>
<button <button
onClick={() => copyToClipboard(otpSecret)} onClick={() => copyToClipboard(otpSecret)}
className="px-2 py-1 text-xs rounded" className="px-2 py-1 text-xs rounded"
style={{ background: '#F0B90B', color: '#000' }} style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}
> >
{t('copy', language)} {t('copy', language)}
</button> </button>
@@ -228,12 +267,12 @@ export function RegisterPage() {
</div> </div>
</div> </div>
<div className="p-3 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}> <div className="p-3 rounded" style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)' }}>
<p className="text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}> <p className="text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
{t('step3Title', language)} {t('authStep3Title', language)}
</p> </p>
<p className="text-xs" style={{ color: '#848E9C' }}> <p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
{t('step3Desc', language)} {t('authStep3Desc', language)}
</p> </p>
</div> </div>
</div> </div>
@@ -259,7 +298,7 @@ export function RegisterPage() {
</div> </div>
<div> <div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}> <label className="block text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
{t('otpCode', language)} {t('otpCode', language)}
</label> </label>
<input <input
@@ -267,7 +306,7 @@ export function RegisterPage() {
value={otpCode} value={otpCode}
onChange={(e) => setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6))} onChange={(e) => setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
className="w-full px-3 py-2 rounded text-center text-2xl font-mono" className="w-full px-3 py-2 rounded text-center text-2xl font-mono"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }}
placeholder={t('otpPlaceholder', language)} placeholder={t('otpPlaceholder', language)}
maxLength={6} maxLength={6}
required required
@@ -275,7 +314,7 @@ export function RegisterPage() {
</div> </div>
{error && ( {error && (
<div className="text-sm px-3 py-2 rounded" style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}> <div className="text-sm px-3 py-2 rounded" style={{ background: 'var(--binance-red-bg)', color: 'var(--binance-red)' }}>
{error} {error}
</div> </div>
)} )}
@@ -285,7 +324,7 @@ export function RegisterPage() {
type="button" type="button"
onClick={() => setStep('setup-otp')} onClick={() => setStep('setup-otp')}
className="flex-1 px-4 py-2 rounded text-sm font-semibold" 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)} {t('back', language)}
</button> </button>
@@ -305,21 +344,22 @@ export function RegisterPage() {
{/* Login Link */} {/* Login Link */}
{step === 'register' && ( {step === 'register' && (
<div className="text-center mt-6"> <div className="text-center mt-6">
<p className="text-sm" style={{ color: '#848E9C' }}> <p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
{' '} {' '}
<button <button
onClick={() => { onClick={() => {
window.history.pushState({}, '', '/login'); window.history.pushState({}, '', '/login');
window.dispatchEvent(new PopStateEvent('popstate')); window.dispatchEvent(new PopStateEvent('popstate'));
}} }}
className="font-semibold hover:underline" className="font-semibold hover:underline transition-colors"
style={{ color: '#F0B90B' }} style={{ color: 'var(--brand-yellow)' }}
> >
</button> </button>
</p> </p>
</div> </div>
)} )}
</div>
</div> </div>
</div> </div>
); );

View File

@@ -1,5 +1,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import type { AIModel, Exchange, CreateTraderRequest } from '../types'; import type { AIModel, Exchange, CreateTraderRequest } from '../types';
import { useLanguage } from '../contexts/LanguageContext';
import { t } from '../i18n/translations';
// 提取下划线后面的名称部分 // 提取下划线后面的名称部分
function getShortName(fullName: string): string { function getShortName(fullName: string): string {
@@ -22,6 +24,7 @@ interface TraderConfigData {
use_coin_pool: boolean; use_coin_pool: boolean;
use_oi_top: boolean; use_oi_top: boolean;
initial_balance: number; initial_balance: number;
scan_interval_minutes: number;
} }
interface TraderConfigModalProps { interface TraderConfigModalProps {
@@ -43,6 +46,7 @@ export function TraderConfigModal({
availableExchanges = [], availableExchanges = [],
onSave onSave
}: TraderConfigModalProps) { }: TraderConfigModalProps) {
const { language } = useLanguage();
const [formData, setFormData] = useState<TraderConfigData>({ const [formData, setFormData] = useState<TraderConfigData>({
trader_name: '', trader_name: '',
ai_model: '', ai_model: '',
@@ -57,6 +61,7 @@ export function TraderConfigModal({
use_coin_pool: false, use_coin_pool: false,
use_oi_top: false, use_oi_top: false,
initial_balance: 1000, initial_balance: 1000,
scan_interval_minutes: 3,
}); });
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [availableCoins, setAvailableCoins] = useState<string[]>([]); const [availableCoins, setAvailableCoins] = useState<string[]>([]);
@@ -87,6 +92,7 @@ export function TraderConfigModal({
use_coin_pool: false, use_coin_pool: false,
use_oi_top: false, use_oi_top: false,
initial_balance: 1000, initial_balance: 1000,
scan_interval_minutes: 3,
}); });
} }
// 确保旧数据也有默认的 system_prompt_template // 确保旧数据也有默认的 system_prompt_template
@@ -181,6 +187,7 @@ export function TraderConfigModal({
use_coin_pool: formData.use_coin_pool, use_coin_pool: formData.use_coin_pool,
use_oi_top: formData.use_oi_top, use_oi_top: formData.use_oi_top,
initial_balance: formData.initial_balance, initial_balance: formData.initial_balance,
scan_interval_minutes: formData.scan_interval_minutes,
}; };
await onSave(saveData); await onSave(saveData);
onClose(); onClose();
@@ -319,7 +326,25 @@ export function TraderConfigModal({
</div> </div>
</div> </div>
{/* 第二行:杠杆设置 */} {/* 第二行:AI 扫描决策间隔 */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm text-[#EAECEF] block mb-2">{t('aiScanInterval', language)}</label>
<input
type="number"
value={formData.scan_interval_minutes}
onChange={(e) => 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"
/>
<p className="text-xs text-gray-500 mt-1">{t('scanIntervalRecommend', language)}</p>
</div>
<div></div>
</div>
{/* 第三行:杠杆设置 */}
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="text-sm text-[#EAECEF] block mb-2">BTC/ETH </label> <label className="text-sm text-[#EAECEF] block mb-2">BTC/ETH </label>

View File

@@ -2,8 +2,13 @@ import { motion } from 'framer-motion'
import { Shield, Target } from 'lucide-react' import { Shield, Target } from 'lucide-react'
import AnimatedSection from './AnimatedSection' import AnimatedSection from './AnimatedSection'
import Typewriter from '../Typewriter' 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 ( return (
<AnimatedSection id='about' backgroundColor='var(--brand-dark-gray)'> <AnimatedSection id='about' backgroundColor='var(--brand-dark-gray)'>
<div className='max-w-7xl mx-auto'> <div className='max-w-7xl mx-auto'>
@@ -31,7 +36,7 @@ export default function AboutSection() {
className='text-sm font-semibold' className='text-sm font-semibold'
style={{ color: 'var(--brand-yellow)' }} style={{ color: 'var(--brand-yellow)' }}
> >
NOFX {t('aboutNofx', language)}
</span> </span>
</motion.div> </motion.div>
@@ -39,23 +44,19 @@ export default function AboutSection() {
className='text-4xl font-bold' className='text-4xl font-bold'
style={{ color: 'var(--brand-light-gray)' }} style={{ color: 'var(--brand-light-gray)' }}
> >
NOFX {t('whatIsNofx', language)}
</h2> </h2>
<p <p
className='text-lg leading-relaxed' className='text-lg leading-relaxed'
style={{ color: 'var(--text-secondary)' }} style={{ color: 'var(--text-secondary)' }}
> >
NOFX AI 'Linux' {t('nofxNotAnotherBot', language)} {t('nofxDescription1', language)} {t('nofxDescription2', language)}
OS '决策-风险-执行'
</p> </p>
<p <p
className='text-lg leading-relaxed' className='text-lg leading-relaxed'
style={{ color: 'var(--text-secondary)' }} style={{ color: 'var(--text-secondary)' }}
> >
24/7AI {t('nofxDescription3', language)} {t('nofxDescription4', language)} {t('nofxDescription5', language)}
CodeFi PR
</p> </p>
<motion.div <motion.div
className='flex items-center gap-3 pt-4' className='flex items-center gap-3 pt-4'
@@ -75,13 +76,13 @@ export default function AboutSection() {
className='font-semibold' className='font-semibold'
style={{ color: 'var(--brand-light-gray)' }} style={{ color: 'var(--brand-light-gray)' }}
> >
100% {t('youFullControl', language)}
</div> </div>
<div <div
className='text-sm' className='text-sm'
style={{ color: 'var(--text-secondary)' }} style={{ color: 'var(--text-secondary)' }}
> >
AI {t('fullControlDesc', language)}
</div> </div>
</div> </div>
</motion.div> </motion.div>
@@ -101,16 +102,16 @@ export default function AboutSection() {
'$ cd nofx', '$ cd nofx',
'$ chmod +x start.sh', '$ chmod +x start.sh',
'$ ./start.sh start --build', '$ ./start.sh start --build',
' 启动自动交易系统...', t('startupMessages1', language),
' API服务器启动在端口 8080', t('startupMessages2', language),
' Web 控制台 http://localhost:3000', t('startupMessages3', language),
]} ]}
typingSpeed={70} typingSpeed={70}
lineDelay={900} lineDelay={900}
className='text-sm font-mono' className='text-sm font-mono'
style={{ style={{
color: '#00FF41', color: '#00FF88',
textShadow: '0 0 6px rgba(0,255,65,0.6)', textShadow: '0 0 8px rgba(0,255,136,0.4)',
}} }}
/> />
</div> </div>

View File

@@ -1,7 +1,16 @@
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import AnimatedSection from './AnimatedSection' 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 ( return (
<motion.div <motion.div
className='p-6 rounded-xl' className='p-6 rounded-xl'
@@ -18,7 +27,7 @@ function TestimonialCard({ quote, author, delay }: any) {
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<div className='w-8 h-8 rounded-full' style={{ background: 'var(--binance-yellow)' }} /> <div className='w-8 h-8 rounded-full' style={{ background: 'var(--binance-yellow)' }} />
<span className='text-sm font-semibold' style={{ color: 'var(--text-secondary)' }}> <span className='text-sm font-semibold' style={{ color: 'var(--text-secondary)' }}>
{author} {authorName}
</span> </span>
</div> </div>
</motion.div> </motion.div>

View File

@@ -2,8 +2,13 @@ import { motion } from 'framer-motion'
import AnimatedSection from './AnimatedSection' import AnimatedSection from './AnimatedSection'
import { CryptoFeatureCard } from '../CryptoFeatureCard' import { CryptoFeatureCard } from '../CryptoFeatureCard'
import { Code, Cpu, Lock, Rocket } from 'lucide-react' 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 ( return (
<AnimatedSection id='features'> <AnimatedSection id='features'>
<div className='max-w-7xl mx-auto'> <div className='max-w-7xl mx-auto'>
@@ -15,37 +20,52 @@ export default function FeaturesSection() {
> >
<Rocket className='w-4 h-4' style={{ color: 'var(--brand-yellow)' }} /> <Rocket className='w-4 h-4' style={{ color: 'var(--brand-yellow)' }} />
<span className='text-sm font-semibold' style={{ color: 'var(--brand-yellow)' }}> <span className='text-sm font-semibold' style={{ color: 'var(--brand-yellow)' }}>
{t('coreFeatures', language)}
</span> </span>
</motion.div> </motion.div>
<h2 className='text-4xl font-bold mb-4' style={{ color: 'var(--brand-light-gray)' }}> <h2 className='text-4xl font-bold mb-4' style={{ color: 'var(--brand-light-gray)' }}>
NOFX {t('whyChooseNofx', language)}
</h2> </h2>
<p className='text-lg' style={{ color: 'var(--text-secondary)' }}> <p className='text-lg' style={{ color: 'var(--text-secondary)' }}>
AI {t('openCommunityDriven', language)}
</p> </p>
</motion.div> </motion.div>
<div className='grid md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-7xl mx-auto'> <div className='grid md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-7xl mx-auto'>
<CryptoFeatureCard <CryptoFeatureCard
icon={<Code className='w-8 h-8' />} icon={<Code className='w-8 h-8' />}
title='100% 开源与自托管' title={t('openSourceSelfHosted', language)}
description='你的框架,你的规则。非黑箱,支持自定义提示词和多模型。' description={t('openSourceDesc', language)}
features={['完全开源代码', '支持自托管部署', '自定义 AI 提示词', '多模型支持DeepSeek、Qwen']} features={[
t('openSourceFeatures1', language),
t('openSourceFeatures2', language),
t('openSourceFeatures3', language),
t('openSourceFeatures4', language)
]}
delay={0} delay={0}
/> />
<CryptoFeatureCard <CryptoFeatureCard
icon={<Cpu className='w-8 h-8' />} icon={<Cpu className='w-8 h-8' />}
title='多代理智能竞争' title={t('multiAgentCompetition', language)}
description='AI 策略在沙盒中高速战斗,最优者生存,实现策略进化。' description={t('multiAgentDesc', language)}
features={['多 AI 代理并行运行', '策略自动优化', '沙盒安全测试', '跨市场策略移植']} features={[
t('multiAgentFeatures1', language),
t('multiAgentFeatures2', language),
t('multiAgentFeatures3', language),
t('multiAgentFeatures4', language)
]}
delay={0.1} delay={0.1}
/> />
<CryptoFeatureCard <CryptoFeatureCard
icon={<Lock className='w-8 h-8' />} icon={<Lock className='w-8 h-8' />}
title='安全可靠交易' title={t('secureReliableTrading', language)}
description='企业级安全保障,完全掌控你的资金和交易策略。' description={t('secureDesc', language)}
features={['本地私钥管理', 'API 权限精细控制', '实时风险监控', '交易日志审计']} features={[
t('secureFeatures1', language),
t('secureFeatures2', language),
t('secureFeatures3', language),
t('secureFeatures4', language)
]}
delay={0.2} delay={0.2}
/> />
</div> </div>

View File

@@ -1,20 +1,22 @@
import { useLanguage } from '../../contexts/LanguageContext' import { t, Language } from '../../i18n/translations'
import { t } from '../../i18n/translations'
export default function FooterSection() { interface FooterSectionProps {
const { language } = useLanguage() language: Language
}
export default function FooterSection({ language }: FooterSectionProps) {
return ( return (
<footer style={{ borderTop: '1px solid #2B3139', background: '#181A20' }}> <footer style={{ borderTop: '1px solid var(--panel-border)', background: 'var(--brand-dark-gray)' }}>
<div className='max-w-[1200px] mx-auto px-6 py-10'> <div className='max-w-[1200px] mx-auto px-6 py-10'>
{/* Brand */} {/* Brand */}
<div className='flex items-center gap-3 mb-8'> <div className='flex items-center gap-3 mb-8'>
<img src='/images/logo.png' alt='NOFX Logo' className='w-8 h-8' /> <img src='/icons/nofx.svg' alt='NOFX Logo' className='w-8 h-8' />
<div> <div>
<div className='text-lg font-bold' style={{ color: '#EAECEF' }}> <div className='text-lg font-bold' style={{ color: '#EAECEF' }}>
NOFX NOFX
</div> </div>
<div className='text-xs' style={{ color: '#848E9C' }}> <div className='text-xs' style={{ color: '#848E9C' }}>
AI {t('futureStandardAI', language)}
</div> </div>
</div> </div>
</div> </div>
@@ -26,7 +28,7 @@ export default function FooterSection() {
className='text-sm font-semibold mb-3' className='text-sm font-semibold mb-3'
style={{ color: '#EAECEF' }} style={{ color: '#EAECEF' }}
> >
{t('links', language)}
</h3> </h3>
<ul className='space-y-2 text-sm' style={{ color: '#848E9C' }}> <ul className='space-y-2 text-sm' style={{ color: '#848E9C' }}>
<li> <li>
@@ -67,7 +69,7 @@ export default function FooterSection() {
className='text-sm font-semibold mb-3' className='text-sm font-semibold mb-3'
style={{ color: '#EAECEF' }} style={{ color: '#EAECEF' }}
> >
{t('resources', language)}
</h3> </h3>
<ul className='space-y-2 text-sm' style={{ color: '#848E9C' }}> <ul className='space-y-2 text-sm' style={{ color: '#848E9C' }}>
<li> <li>
@@ -77,7 +79,7 @@ export default function FooterSection() {
target='_blank' target='_blank'
rel='noopener noreferrer' rel='noopener noreferrer'
> >
{t('documentation', language)}
</a> </a>
</li> </li>
<li> <li>
@@ -108,7 +110,7 @@ export default function FooterSection() {
className='text-sm font-semibold mb-3' className='text-sm font-semibold mb-3'
style={{ color: '#EAECEF' }} style={{ color: '#EAECEF' }}
> >
{t('supporters', language)}
</h3> </h3>
<ul className='space-y-2 text-sm' style={{ color: '#848E9C' }}> <ul className='space-y-2 text-sm' style={{ color: '#848E9C' }}>
<li> <li>
@@ -148,7 +150,7 @@ export default function FooterSection() {
target='_blank' target='_blank'
rel='noopener noreferrer' rel='noopener noreferrer'
> >
Amber.ac <span className='opacity-70'>()</span> Amber.ac <span className='opacity-70'>{t('strategicInvestment', language)}</span>
</a> </a>
</li> </li>
</ul> </ul>
@@ -158,7 +160,7 @@ export default function FooterSection() {
{/* Bottom note (kept subtle) */} {/* Bottom note (kept subtle) */}
<div <div
className='pt-6 mt-8 text-center text-xs' className='pt-6 mt-8 text-center text-xs'
style={{ color: '#5E6673', borderTop: '1px solid #2B3139' }} style={{ color: 'var(--text-tertiary)', borderTop: '1px solid var(--panel-border)' }}
> >
<p>{t('footerTitle', language)}</p> <p>{t('footerTitle', language)}</p>
<p className='mt-1'>{t('footerWarning', language)}</p> <p className='mt-1'>{t('footerWarning', language)}</p>

View File

@@ -1,56 +1,363 @@
import { useState } from 'react' import { useState, useEffect, useRef } from 'react'
import { motion } from 'framer-motion' 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 [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const [languageDropdownOpen, setLanguageDropdownOpen] = useState(false)
const [userDropdownOpen, setUserDropdownOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
const userDropdownRef = useRef<HTMLDivElement>(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 ( return (
<nav className='fixed top-0 w-full z-50 header-bar'> <nav className='fixed top-0 w-full z-50 header-bar'>
<div className='max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'> <div className='max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'>
<div className='flex items-center justify-between h-16'> <div className='flex items-center justify-between h-16'>
{/* Logo */} {/* Logo */}
<div className='flex items-center gap-3'> <a href='/' className='flex items-center gap-3 hover:opacity-80 transition-opacity cursor-pointer'>
<img src='/images/logo.png' alt='NOFX Logo' className='w-8 h-8' /> <img src='/icons/nofx.svg' alt='NOFX Logo' className='w-8 h-8' />
<span className='text-xl font-bold' style={{ color: 'var(--brand-yellow)' }}> <span className='text-xl font-bold' style={{ color: 'var(--brand-yellow)' }}>
NOFX NOFX
</span> </span>
<span className='text-sm hidden sm:block' style={{ color: 'var(--text-secondary)' }}> <span className='text-sm hidden sm:block' style={{ color: 'var(--text-secondary)' }}>
Agentic Trading OS Agentic Trading OS
</span> </span>
</div> </a>
{/* Desktop Menu */} {/* Desktop Menu */}
<div className='hidden md:flex items-center gap-6'> <div className='hidden md:flex items-center justify-between flex-1 ml-8'>
{['功能', '如何运作', 'GitHub', '社区'].map((item) => ( {/* Left Side - Navigation Tabs */}
<a <div className='flex items-center gap-4'>
key={item} {isLoggedIn ? (
href={ // Main app navigation when logged in
item === 'GitHub' <>
? 'https://github.com/tinkle-community/nofx' <button
: item === '社区' onClick={() => {
? 'https://t.me/nofx_dev_community' console.log('实时 button clicked, onPageChange:', onPageChange);
: `#${item === '功能' ? 'features' : 'how-it-works'}` onPageChange?.('competition');
} }}
target={item === 'GitHub' || item === '社区' ? '_blank' : undefined} className='text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500'
rel={item === 'GitHub' || item === '社区' ? 'noopener noreferrer' : undefined} style={{
className='text-sm transition-colors relative group' color: currentPage === 'competition' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
style={{ color: 'var(--brand-light-gray)' }} padding: '8px 16px',
> borderRadius: '8px',
{item} position: 'relative'
<span }}
className='absolute -bottom-1 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300' onMouseEnter={(e) => {
style={{ background: 'var(--brand-yellow)' }} if (currentPage !== 'competition') {
/> e.currentTarget.style.color = 'var(--brand-yellow)';
</a> }
))} }}
<button onMouseLeave={(e) => {
onClick={onLoginClick} if (currentPage !== 'competition') {
className='px-4 py-2 rounded font-semibold text-sm' e.currentTarget.style.color = 'var(--brand-light-gray)';
style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }} }
> }}
/ >
</button> {/* Background for selected state */}
{currentPage === 'competition' && (
<span
className="absolute inset-0 rounded-lg"
style={{
background: 'rgba(240, 185, 11, 0.15)',
zIndex: -1
}}
/>
)}
{t('realtimeNav', language)}
</button>
<button
onClick={() => {
console.log('配置 button clicked, onPageChange:', onPageChange);
onPageChange?.('traders');
}}
className='text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500'
style={{
color: currentPage === 'traders' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
padding: '8px 16px',
borderRadius: '8px',
position: 'relative'
}}
onMouseEnter={(e) => {
if (currentPage !== 'traders') {
e.currentTarget.style.color = 'var(--brand-yellow)';
}
}}
onMouseLeave={(e) => {
if (currentPage !== 'traders') {
e.currentTarget.style.color = 'var(--brand-light-gray)';
}
}}
>
{/* Background for selected state */}
{currentPage === 'traders' && (
<span
className="absolute inset-0 rounded-lg"
style={{
background: 'rgba(240, 185, 11, 0.15)',
zIndex: -1
}}
/>
)}
{t('configNav', language)}
</button>
<button
onClick={() => {
console.log('看板 button clicked, onPageChange:', onPageChange);
onPageChange?.('trader');
}}
className='text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500'
style={{
color: currentPage === 'trader' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
padding: '8px 16px',
borderRadius: '8px',
position: 'relative'
}}
onMouseEnter={(e) => {
if (currentPage !== 'trader') {
e.currentTarget.style.color = 'var(--brand-yellow)';
}
}}
onMouseLeave={(e) => {
if (currentPage !== 'trader') {
e.currentTarget.style.color = 'var(--brand-light-gray)';
}
}}
>
{/* Background for selected state */}
{currentPage === 'trader' && (
<span
className="absolute inset-0 rounded-lg"
style={{
background: 'rgba(240, 185, 11, 0.15)',
zIndex: -1
}}
/>
)}
{t('dashboardNav', language)}
</button>
</>
) : (
// Landing page navigation when not logged in
<a
href='/competition'
className='text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500'
style={{
color: currentPage === 'competition' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
padding: '8px 16px',
borderRadius: '8px',
position: 'relative'
}}
onMouseEnter={(e) => {
if (currentPage !== 'competition') {
e.currentTarget.style.color = 'var(--brand-yellow)';
}
}}
onMouseLeave={(e) => {
if (currentPage !== 'competition') {
e.currentTarget.style.color = 'var(--brand-light-gray)';
}
}}
>
{/* Background for selected state */}
{currentPage === 'competition' && (
<span
className="absolute inset-0 rounded-lg"
style={{
background: 'rgba(240, 185, 11, 0.15)',
zIndex: -1
}}
/>
)}
{t('realtimeNav', language)}
</a>
)}
</div>
{/* Right Side - Original Navigation Items and Login */}
<div className='flex items-center gap-6'>
{/* Only show original navigation items on home page */}
{isHomePage && [
{ key: 'features', label: t('features', language) },
{ key: 'howItWorks', label: t('howItWorks', language) },
{ key: 'GitHub', label: 'GitHub' },
{ key: 'community', label: t('community', language) }
].map((item) => (
<a
key={item.key}
href={
item.key === 'GitHub'
? 'https://github.com/tinkle-community/nofx'
: item.key === 'community'
? 'https://t.me/nofx_dev_community'
: `#${item.key === 'features' ? 'features' : 'how-it-works'}`
}
target={item.key === 'GitHub' || item.key === 'community' ? '_blank' : undefined}
rel={item.key === 'GitHub' || item.key === 'community' ? 'noopener noreferrer' : undefined}
className='text-sm transition-colors relative group'
style={{ color: 'var(--brand-light-gray)' }}
>
{item.label}
<span
className='absolute -bottom-1 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300'
style={{ background: 'var(--brand-yellow)' }}
/>
</a>
))}
{/* User Info and Actions */}
{isLoggedIn && user ? (
<div className='flex items-center gap-3'>
{/* User Info with Dropdown */}
<div className='relative' ref={userDropdownRef}>
<button
onClick={() => setUserDropdownOpen(!userDropdownOpen)}
className='flex items-center gap-2 px-3 py-2 rounded transition-colors'
style={{ background: 'var(--panel-bg)', border: '1px solid var(--panel-border)' }}
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255, 255, 255, 0.05)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'var(--panel-bg)'}
>
<div className='w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold' style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}>
{user.email[0].toUpperCase()}
</div>
<span className='text-sm' style={{ color: 'var(--brand-light-gray)' }}>{user.email}</span>
<ChevronDown className='w-4 h-4' style={{ color: 'var(--brand-light-gray)' }} />
</button>
{userDropdownOpen && (
<div className='absolute right-0 top-full mt-2 w-48 rounded-lg shadow-lg overflow-hidden z-50' style={{ background: 'var(--brand-dark-gray)', border: '1px solid var(--panel-border)' }}>
<div className='px-3 py-2 border-b' style={{ borderColor: 'var(--panel-border)' }}>
<div className='text-xs' style={{ color: 'var(--text-secondary)' }}>{t('loggedInAs', language)}</div>
<div className='text-sm font-medium' style={{ color: 'var(--brand-light-gray)' }}>{user.email}</div>
</div>
{!isAdminMode && onLogout && (
<button
onClick={() => {
onLogout()
setUserDropdownOpen(false)
}}
className='w-full px-3 py-2 text-sm font-semibold transition-colors hover:opacity-80 text-center'
style={{ background: 'var(--binance-red-bg)', color: 'var(--binance-red)' }}
>
{t('exitLogin', language)}
</button>
)}
</div>
)}
</div>
</div>
) : (
/* Show login/register buttons when not logged in and not on login/register pages */
currentPage !== 'login' && currentPage !== 'register' && (
<div className='flex items-center gap-3'>
<a
href='/login'
className='px-3 py-2 text-sm font-medium transition-colors rounded'
style={{ color: 'var(--brand-light-gray)' }}
>
{t('signIn', language)}
</a>
<a
href='/register'
className='px-4 py-2 rounded font-semibold text-sm transition-colors hover:opacity-90'
style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}
>
{t('signUp', language)}
</a>
</div>
)
)}
{/* Language Toggle - Always at the rightmost */}
<div className='relative' ref={dropdownRef}>
<button
onClick={() => setLanguageDropdownOpen(!languageDropdownOpen)}
className='flex items-center gap-2 px-3 py-2 rounded transition-colors'
style={{ color: 'var(--brand-light-gray)' }}
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255, 255, 255, 0.05)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
>
<span className='text-lg'>
{language === 'zh' ? '🇨🇳' : '🇺🇸'}
</span>
<ChevronDown className='w-4 h-4' />
</button>
{languageDropdownOpen && (
<div className='absolute right-0 top-full mt-2 w-32 rounded-lg shadow-lg overflow-hidden z-50' style={{ background: 'var(--brand-dark-gray)', border: '1px solid var(--panel-border)' }}>
<button
onClick={() => {
onLanguageChange?.('zh')
setLanguageDropdownOpen(false)
}}
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors ${
language === 'zh' ? '' : 'hover:opacity-80'
}`}
style={{
color: 'var(--brand-light-gray)',
background: language === 'zh' ? 'rgba(240, 185, 11, 0.1)' : 'transparent'
}}
>
<span className='text-base'>🇨🇳</span>
<span className='text-sm'></span>
</button>
<button
onClick={() => {
onLanguageChange?.('en')
setLanguageDropdownOpen(false)
}}
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors ${
language === 'en' ? '' : 'hover:opacity-80'
}`}
style={{
color: 'var(--brand-light-gray)',
background: language === 'en' ? 'rgba(240, 185, 11, 0.1)' : 'transparent'
}}
>
<span className='text-base'>🇺🇸</span>
<span className='text-sm'>English</span>
</button>
</div>
)}
</div>
</div>
</div> </div>
{/* Mobile Menu Button */} {/* Mobile Menu Button */}
@@ -74,21 +381,232 @@ export default function HeaderBar({ onLoginClick }: { onLoginClick: () => void }
style={{ background: 'var(--brand-dark-gray)', borderTop: '1px solid rgba(240, 185, 11, 0.1)' }} style={{ background: 'var(--brand-dark-gray)', borderTop: '1px solid rgba(240, 185, 11, 0.1)' }}
> >
<div className='px-4 py-4 space-y-3'> <div className='px-4 py-4 space-y-3'>
{['功能', '如何运作', 'GitHub', '社区'].map((item) => ( {/* New Navigation Tabs */}
<a key={item} href={`#${item}`} className='block text-sm py-2' style={{ color: 'var(--brand-light-gray)' }}> {isLoggedIn ? (
{item} <button
onClick={() => {
console.log('移动端 实时 button clicked, onPageChange:', onPageChange);
onPageChange?.('competition')
setMobileMenuOpen(false)
}}
className='block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500'
style={{
color: currentPage === 'competition' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
padding: '12px 16px',
borderRadius: '8px',
position: 'relative',
width: '100%',
textAlign: 'left'
}}
>
{/* Background for selected state */}
{currentPage === 'competition' && (
<span
className="absolute inset-0 rounded-lg"
style={{
background: 'rgba(240, 185, 11, 0.15)',
zIndex: -1
}}
/>
)}
{t('realtimeNav', language)}
</button>
) : (
<a
href='/competition'
className='block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500'
style={{
color: currentPage === 'competition' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
padding: '12px 16px',
borderRadius: '8px',
position: 'relative'
}}
>
{/* Background for selected state */}
{currentPage === 'competition' && (
<span
className="absolute inset-0 rounded-lg"
style={{
background: 'rgba(240, 185, 11, 0.15)',
zIndex: -1
}}
/>
)}
{t('realtimeNav', language)}
</a>
)}
{/* Only show 配置 and 看板 when logged in */}
{isLoggedIn && (
<>
<button
onClick={() => {
console.log('移动端 配置 button clicked, onPageChange:', onPageChange);
onPageChange?.('traders')
setMobileMenuOpen(false)
}}
className='block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 hover:text-yellow-500'
style={{
color: currentPage === 'traders' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
padding: '12px 16px',
borderRadius: '8px',
position: 'relative',
width: '100%',
textAlign: 'left'
}}
>
{/* Background for selected state */}
{currentPage === 'traders' && (
<span
className="absolute inset-0 rounded-lg"
style={{
background: 'rgba(240, 185, 11, 0.15)',
zIndex: -1
}}
/>
)}
{t('configNav', language)}
</button>
<button
onClick={() => {
console.log('移动端 看板 button clicked, onPageChange:', onPageChange);
onPageChange?.('trader')
setMobileMenuOpen(false)
}}
className='block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 hover:text-yellow-500'
style={{
color: currentPage === 'trader' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
padding: '12px 16px',
borderRadius: '8px',
position: 'relative',
width: '100%',
textAlign: 'left'
}}
>
{/* Background for selected state */}
{currentPage === 'trader' && (
<span
className="absolute inset-0 rounded-lg"
style={{
background: 'rgba(240, 185, 11, 0.15)',
zIndex: -1
}}
/>
)}
{t('dashboardNav', language)}
</button>
</>
)}
{/* Original Navigation Items - Only on home page */}
{isHomePage && [
{ key: 'features', label: t('features', language) },
{ key: 'howItWorks', label: t('howItWorks', language) },
{ key: 'GitHub', label: 'GitHub' },
{ key: 'community', label: t('community', language) }
].map((item) => (
<a
key={item.key}
href={
item.key === 'GitHub'
? 'https://github.com/tinkle-community/nofx'
: item.key === 'community'
? 'https://t.me/nofx_dev_community'
: `#${item.key === 'features' ? 'features' : 'how-it-works'}`
}
target={item.key === 'GitHub' || item.key === 'community' ? '_blank' : undefined}
rel={item.key === 'GitHub' || item.key === 'community' ? 'noopener noreferrer' : undefined}
className='block text-sm py-2'
style={{ color: 'var(--brand-light-gray)' }}
>
{item.label}
</a> </a>
))} ))}
<button
onClick={() => { {/* Language Toggle */}
onLoginClick() <div className='py-2'>
setMobileMenuOpen(false) <div className='flex items-center gap-2 mb-2'>
}} <span className='text-xs' style={{ color: 'var(--brand-light-gray)' }}>{t('language', language)}:</span>
className='w-full px-4 py-2 rounded font-semibold text-sm mt-2' </div>
style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }} <div className='space-y-1'>
> <button
/ onClick={() => {
</button> onLanguageChange?.('zh')
setMobileMenuOpen(false)
}}
className={`w-full flex items-center gap-3 px-3 py-2 rounded transition-colors ${
language === 'zh' ? 'bg-yellow-500 text-black' : 'text-gray-400 hover:text-white'
}`}
>
<span className='text-lg'>🇨🇳</span>
<span className='text-sm'></span>
</button>
<button
onClick={() => {
onLanguageChange?.('en')
setMobileMenuOpen(false)
}}
className={`w-full flex items-center gap-3 px-3 py-2 rounded transition-colors ${
language === 'en' ? 'bg-yellow-500 text-black' : 'text-gray-400 hover:text-white'
}`}
>
<span className='text-lg'>🇺🇸</span>
<span className='text-sm'>English</span>
</button>
</div>
</div>
{/* User info and logout for mobile when logged in */}
{isLoggedIn && user && (
<div className='mt-4 pt-4' style={{ borderTop: '1px solid var(--panel-border)' }}>
<div className='flex items-center gap-2 px-3 py-2 mb-2 rounded' style={{ background: 'var(--panel-bg)' }}>
<div className='w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold' style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}>
{user.email[0].toUpperCase()}
</div>
<div>
<div className='text-xs' style={{ color: 'var(--text-secondary)' }}>{t('loggedInAs', language)}</div>
<div className='text-sm' style={{ color: 'var(--brand-light-gray)' }}>{user.email}</div>
</div>
</div>
{!isAdminMode && onLogout && (
<button
onClick={() => {
onLogout()
setMobileMenuOpen(false)
}}
className='w-full px-4 py-2 rounded text-sm font-semibold transition-colors text-center'
style={{ background: 'var(--binance-red-bg)', color: 'var(--binance-red)' }}
>
{t('exitLogin', language)}
</button>
)}
</div>
)}
{/* Show login/register buttons when not logged in and not on login/register pages */}
{!isLoggedIn && currentPage !== 'login' && currentPage !== 'register' && (
<div className='space-y-2 mt-2'>
<a
href='/login'
className='block w-full px-4 py-2 rounded text-sm font-medium text-center transition-colors'
style={{ color: 'var(--brand-light-gray)', border: '1px solid var(--brand-light-gray)' }}
onClick={() => setMobileMenuOpen(false)}
>
{t('signIn', language)}
</a>
<a
href='/register'
className='block w-full px-4 py-2 rounded font-semibold text-sm text-center transition-colors'
style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}
onClick={() => setMobileMenuOpen(false)}
>
{t('signUp', language)}
</a>
</div>
)}
</div> </div>
</motion.div> </motion.div>
</nav> </nav>

View File

@@ -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 { 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 { scrollYProgress } = useScroll()
const opacity = useTransform(scrollYProgress, [0, 0.2], [1, 0]) const opacity = useTransform(scrollYProgress, [0, 0.2], [1, 0])
const scale = useTransform(scrollYProgress, [0, 0.2], [1, 0.8]) const scale = useTransform(scrollYProgress, [0, 0.2], [1, 0.8])
const handControls = useAnimation()
const fadeInUp = { const fadeInUp = {
initial: { opacity: 0, y: 60 }, initial: { opacity: 0, y: 60 },
@@ -27,40 +33,39 @@ export default function HeroSection() {
> >
<Sparkles className='w-4 h-4' style={{ color: 'var(--brand-yellow)' }} /> <Sparkles className='w-4 h-4' style={{ color: 'var(--brand-yellow)' }} />
<span className='text-sm font-semibold' style={{ color: 'var(--brand-yellow)' }}> <span className='text-sm font-semibold' style={{ color: 'var(--brand-yellow)' }}>
3 2.5K+ GitHub Stars {t('githubStarsInDays', language)}
</span> </span>
</motion.div> </motion.div>
</motion.div> </motion.div>
<h1 className='text-5xl lg:text-7xl font-bold leading-tight' style={{ color: 'var(--brand-light-gray)' }}> <h1 className='text-5xl lg:text-7xl font-bold leading-tight' style={{ color: 'var(--brand-light-gray)' }}>
Read the Market. {t('heroTitle1', language)}
<br /> <br />
<span style={{ color: 'var(--brand-yellow)' }}>Write the Trade.</span> <span style={{ color: 'var(--brand-yellow)' }}>{t('heroTitle2', language)}</span>
</h1> </h1>
<motion.p className='text-xl leading-relaxed' style={{ color: 'var(--text-secondary)' }} variants={fadeInUp}> <motion.p className='text-xl leading-relaxed' style={{ color: 'var(--text-secondary)' }} variants={fadeInUp}>
NOFX AI BinanceAster DEX {t('heroDescription', language)}
AI
</motion.p> </motion.p>
<div className='flex items-center gap-3 flex-wrap'> <div className='flex items-center gap-3 flex-wrap'>
<motion.a href='https://github.com/tinkle-community/nofx' target='_blank' rel='noopener noreferrer' whileHover={{ scale: 1.05 }} transition={{ type: 'spring', stiffness: 400 }}> <motion.a href='https://github.com/tinkle-community/nofx' target='_blank' rel='noopener noreferrer' whileHover={{ scale: 1.05 }} transition={{ type: 'spring', stiffness: 400 }}>
<img <img
src='https://img.shields.io/github/stars/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=1E2329' src='https://img.shields.io/github/stars/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=0A0A0A'
alt='GitHub Stars' alt='GitHub Stars'
className='h-7' className='h-7'
/> />
</motion.a> </motion.a>
<motion.a href='https://github.com/tinkle-community/nofx/network/members' target='_blank' rel='noopener noreferrer' whileHover={{ scale: 1.05 }} transition={{ type: 'spring', stiffness: 400 }}> <motion.a href='https://github.com/tinkle-community/nofx/network/members' target='_blank' rel='noopener noreferrer' whileHover={{ scale: 1.05 }} transition={{ type: 'spring', stiffness: 400 }}>
<img <img
src='https://img.shields.io/github/forks/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=1E2329' src='https://img.shields.io/github/forks/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=0A0A0A'
alt='GitHub Forks' alt='GitHub Forks'
className='h-7' className='h-7'
/> />
</motion.a> </motion.a>
<motion.a href='https://github.com/tinkle-community/nofx/graphs/contributors' target='_blank' rel='noopener noreferrer' whileHover={{ scale: 1.05 }} transition={{ type: 'spring', stiffness: 400 }}> <motion.a href='https://github.com/tinkle-community/nofx/graphs/contributors' target='_blank' rel='noopener noreferrer' whileHover={{ scale: 1.05 }} transition={{ type: 'spring', stiffness: 400 }}>
<img <img
src='https://img.shields.io/github/contributors/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=1E2329' src='https://img.shields.io/github/contributors/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=0A0A0A'
alt='GitHub Contributors' alt='GitHub Contributors'
className='h-7' className='h-7'
/> />
@@ -68,12 +73,62 @@ export default function HeroSection() {
</div> </div>
<motion.p className='text-xs pt-4' style={{ color: 'var(--text-tertiary)' }} variants={fadeInUp}> <motion.p className='text-xs pt-4' style={{ color: 'var(--text-tertiary)' }} variants={fadeInUp}>
Aster DEX Binance Amber.ac {t('poweredBy', language)}
</motion.p> </motion.p>
</motion.div> </motion.div>
{/* Right Visual */} {/* Right Visual - Interactive Robot */}
<motion.img src='/images/main.png' alt='NOFX Platform' className='w-full opacity-90' whileHover={{ scale: 1.05, rotate: 5 }} transition={{ type: 'spring', stiffness: 300 }} /> <div
className='relative w-full cursor-pointer'
onMouseEnter={() => {
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 */}
<motion.img
src='/images/hand-bg.png'
alt='NOFX Platform Background'
className='w-full opacity-90'
style={{ opacity, scale }}
whileHover={{ scale: 1.02 }}
transition={{ type: 'spring', stiffness: 300 }}
/>
{/* Hand Layer - Animated */}
<motion.img
src='/images/hand.png'
alt='Robot Hand'
className='absolute top-0 left-0 w-full'
style={{ opacity }}
animate={handControls}
initial={{ y: 0, rotate: 0, x: 0 }}
whileHover={{
scale: 1.05,
transition: { type: 'spring', stiffness: 400 }
}}
/>
</div>
</div> </div>
</div> </div>
</section> </section>

View File

@@ -1,5 +1,6 @@
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import AnimatedSection from './AnimatedSection' import AnimatedSection from './AnimatedSection'
import { t, Language } from '../../i18n/translations'
function StepCard({ number, title, description, delay }: any) { function StepCard({ number, title, description, delay }: any) {
return ( 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 ( return (
<AnimatedSection id='how-it-works' backgroundColor='var(--brand-dark-gray)'> <AnimatedSection id='how-it-works' backgroundColor='var(--brand-dark-gray)'>
<div className='max-w-7xl mx-auto'> <div className='max-w-7xl mx-auto'>
<motion.div className='text-center mb-16' initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }}> <motion.div className='text-center mb-16' initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }}>
<h2 className='text-4xl font-bold mb-4' style={{ color: 'var(--brand-light-gray)' }}> <h2 className='text-4xl font-bold mb-4' style={{ color: 'var(--brand-light-gray)' }}>
使 NOFX {t('howToStart', language)}
</h2> </h2>
<p className='text-lg' style={{ color: 'var(--text-secondary)' }}> <p className='text-lg' style={{ color: 'var(--text-secondary)' }}>
AI {t('fourSimpleSteps', language)}
</p> </p>
</motion.div> </motion.div>
<div className='space-y-8'> <div className='space-y-8'>
{[ {[
{ number: 1, title: '拉取 GitHub 仓库', description: 'git clone https://github.com/tinkle-community/nofx 并切换到 dev 分支测试新功能。' }, { number: 1, title: t('step1Title', language), description: t('step1Desc', language) },
{ number: 2, title: '配置环境', description: '前端设置交易所 API如 Binance、Hyperliquid、AI 模型和自定义提示词。' }, { number: 2, title: t('step2Title', language), description: t('step2Desc', language) },
{ number: 3, title: '部署与运行', description: '一键 Docker 部署,启动 AI 代理。注意:高风险市场,仅用闲钱测试。' }, { number: 3, title: t('step3Title', language), description: t('step3Desc', language) },
{ number: 4, title: '优化与贡献', description: '监控交易,提交 PR 改进框架。加入 Telegram 分享策略。' }, { number: 4, title: t('step4Title', language), description: t('step4Desc', language) },
].map((step, index) => ( ].map((step, index) => (
<StepCard key={step.number} {...step} delay={index * 0.1} /> <StepCard key={step.number} {...step} delay={index * 0.1} />
))} ))}
@@ -61,10 +66,10 @@ export default function HowItWorksSection() {
</div> </div>
<div> <div>
<div className='font-semibold mb-2' style={{ color: '#F6465D' }}> <div className='font-semibold mb-2' style={{ color: '#F6465D' }}>
{t('importantRiskWarning', language)}
</div> </div>
<p className='text-sm' style={{ color: 'var(--text-secondary)' }}> <p className='text-sm' style={{ color: 'var(--text-secondary)' }}>
dev NOFX {t('riskWarningText', language)}
</p> </p>
</div> </div>
</motion.div> </motion.div>

View File

@@ -1,7 +1,13 @@
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import { X } from 'lucide-react' 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 ( return (
<motion.div <motion.div
className='fixed inset-0 z-50 flex items-center justify-center p-4' className='fixed inset-0 z-50 flex items-center justify-center p-4'
@@ -23,10 +29,10 @@ export default function LoginModal({ onClose }: { onClose: () => void }) {
<X className='w-6 h-6' /> <X className='w-6 h-6' />
</motion.button> </motion.button>
<h2 className='text-2xl font-bold mb-6' style={{ color: 'var(--brand-light-gray)' }}> <h2 className='text-2xl font-bold mb-6' style={{ color: 'var(--brand-light-gray)' }}>
访 NOFX {t('accessNofxPlatform', language)}
</h2> </h2>
<p className='text-sm mb-6' style={{ color: 'var(--text-secondary)' }}> <p className='text-sm mb-6' style={{ color: 'var(--text-secondary)' }}>
访 AI {t('loginRegisterPrompt', language)}
</p> </p>
<div className='space-y-3'> <div className='space-y-3'>
<motion.button <motion.button
@@ -40,7 +46,7 @@ export default function LoginModal({ onClose }: { onClose: () => void }) {
whileHover={{ scale: 1.05, boxShadow: '0 10px 30px rgba(240, 185, 11, 0.4)' }} whileHover={{ scale: 1.05, boxShadow: '0 10px 30px rgba(240, 185, 11, 0.4)' }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
> >
{t('signIn', language)}
</motion.button> </motion.button>
<motion.button <motion.button
onClick={() => { onClick={() => {
@@ -53,7 +59,7 @@ export default function LoginModal({ onClose }: { onClose: () => void }) {
whileHover={{ scale: 1.05, borderColor: 'var(--brand-yellow)' }} whileHover={{ scale: 1.05, borderColor: 'var(--brand-yellow)' }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
> >
{t('registerNewAccount', language)}
</motion.button> </motion.button>
</div> </div>
</motion.div> </motion.div>

View File

@@ -10,7 +10,7 @@ interface AuthContextType {
user: User | null; user: User | null;
token: string | null; token: string | null;
login: (email: string, password: string) => Promise<{ success: boolean; message?: string; userID?: string; requiresOTP?: boolean }>; 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 }>; verifyOTP: (userID: string, otpCode: string) => Promise<{ success: boolean; message?: string }>;
completeRegistration: (userID: string, otpCode: string) => Promise<{ success: boolean; message?: string }>; completeRegistration: (userID: string, otpCode: string) => Promise<{ success: boolean; message?: string }>;
logout: () => void; logout: () => void;
@@ -89,14 +89,19 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
return { success: false, message: '未知错误' }; return { success: false, message: '未知错误' };
}; };
const register = async (email: string, password: string) => { const register = async (email: string, password: string, betaCode?: string) => {
try { try {
const requestBody: { email: string; password: string; beta_code?: string } = { email, password };
if (betaCode) {
requestBody.beta_code = betaCode;
}
const response = await fetch('/api/register', { const response = await fetch('/api/register', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ email, password }), body: JSON.stringify(requestBody),
}); });
const data = await response.json(); const data = await response.json();

View File

@@ -15,6 +15,11 @@ export const translations = {
logout: 'Logout', logout: 'Logout',
switchTrader: 'Switch Trader:', switchTrader: 'Switch Trader:',
view: 'View', view: 'View',
// Navigation
realtimeNav: 'Live',
configNav: 'Config',
dashboardNav: 'Dashboard',
// Footer // Footer
footerTitle: 'NOFX - AI Trading System', footerTitle: 'NOFX - AI Trading System',
@@ -158,6 +163,8 @@ export const translations = {
create: 'Create', create: 'Create',
configureAIModels: 'Configure AI Models', configureAIModels: 'Configure AI Models',
configureExchanges: 'Configure Exchanges', configureExchanges: 'Configure Exchanges',
aiScanInterval: 'AI Scan Decision Interval (minutes)',
scanIntervalRecommend: 'Recommended: 3-10 minutes',
useTestnet: 'Use Testnet', useTestnet: 'Use Testnet',
enabled: 'Enabled', enabled: 'Enabled',
save: 'Save', save: 'Save',
@@ -296,12 +303,12 @@ export const translations = {
scanQRCodeInstructions: 'Scan this QR code with Google Authenticator or Authy', scanQRCodeInstructions: 'Scan this QR code with Google Authenticator or Authy',
otpSecret: 'Or enter this secret manually:', otpSecret: 'Or enter this secret manually:',
qrCodeHint: 'QR code (if scanning fails, use the secret below):', qrCodeHint: 'QR code (if scanning fails, use the secret below):',
step1Title: 'Step 1: Install Google Authenticator', authStep1Title: 'Step 1: Install Google Authenticator',
step1Desc: 'Download and install Google Authenticator from your app store', authStep1Desc: 'Download and install Google Authenticator from your app store',
step2Title: 'Step 2: Add account', authStep2Title: 'Step 2: Add account',
step2Desc: 'Tap "+", then choose "Scan QR code" or "Enter a setup key"', authStep2Desc: 'Tap "+", then choose "Scan QR code" or "Enter a setup key"',
step3Title: 'Step 3: Verify setup', authStep3Title: 'Step 3: Verify setup',
step3Desc: 'After setup, continue to enter the 6-digit code', authStep3Desc: 'After setup, continue to enter the 6-digit code',
setupCompleteContinue: 'I have completed setup, continue', setupCompleteContinue: 'I have completed setup, continue',
copy: 'Copy', copy: 'Copy',
completeRegistration: 'Complete Registration', completeRegistration: 'Complete Registration',
@@ -317,6 +324,96 @@ export const translations = {
passwordRequired: 'Password is required', passwordRequired: 'Password is required',
invalidEmail: 'Invalid email format', invalidEmail: 'Invalid email format',
passwordTooShort: 'Password must be at least 6 characters', 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: { zh: {
// Header // Header
@@ -332,6 +429,11 @@ export const translations = {
logout: '退出', logout: '退出',
switchTrader: '切换交易员:', switchTrader: '切换交易员:',
view: '查看', view: '查看',
// Navigation
realtimeNav: '实时',
configNav: '配置',
dashboardNav: '看板',
// Footer // Footer
footerTitle: 'NOFX - AI交易系统', footerTitle: 'NOFX - AI交易系统',
@@ -475,6 +577,8 @@ export const translations = {
create: '创建', create: '创建',
configureAIModels: '配置AI模型', configureAIModels: '配置AI模型',
configureExchanges: '配置交易所', configureExchanges: '配置交易所',
aiScanInterval: 'AI 扫描决策间隔 (分钟)',
scanIntervalRecommend: '建议: 3-10分钟',
useTestnet: '使用测试网', useTestnet: '使用测试网',
enabled: '启用', enabled: '启用',
save: '保存', save: '保存',
@@ -613,12 +717,12 @@ export const translations = {
scanQRCodeInstructions: '使用Google Authenticator或Authy扫描此二维码', scanQRCodeInstructions: '使用Google Authenticator或Authy扫描此二维码',
otpSecret: '或手动输入此密钥:', otpSecret: '或手动输入此密钥:',
qrCodeHint: '二维码(如果无法扫描,请使用下方密钥):', qrCodeHint: '二维码(如果无法扫描,请使用下方密钥):',
step1Title: '步骤1下载Google Authenticator', authStep1Title: '步骤1下载Google Authenticator',
step1Desc: '在手机应用商店下载并安装Google Authenticator应用', authStep1Desc: '在手机应用商店下载并安装Google Authenticator应用',
step2Title: '步骤2添加账户', authStep2Title: '步骤2添加账户',
step2Desc: '在应用中点击“+”,选择“扫描二维码”或“手动输入密钥”', authStep2Desc: '在应用中点击“+”,选择“扫描二维码”或“手动输入密钥”',
step3Title: '步骤3验证设置', authStep3Title: '步骤3验证设置',
step3Desc: '设置完成后点击下方按钮输入6位验证码', authStep3Desc: '设置完成后点击下方按钮输入6位验证码',
setupCompleteContinue: '我已完成设置,继续', setupCompleteContinue: '我已完成设置,继续',
copy: '复制', copy: '复制',
completeRegistration: '完成注册', completeRegistration: '完成注册',
@@ -634,6 +738,96 @@ export const translations = {
passwordRequired: '请输入密码', passwordRequired: '请输入密码',
invalidEmail: '邮箱格式不正确', invalidEmail: '邮箱格式不正确',
passwordTooShort: '密码至少需要6个字符', 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: '注册新账号',
} }
}; };

View File

@@ -5,11 +5,16 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* 强制显示滚动条以确保所有页面布局一致 */
html {
overflow-y: scroll;
}
:root { :root {
/* Binance Brand Colors */ /* Binance Brand Colors */
--brand-yellow: #F0B90B; --brand-yellow: #F0B90B;
--brand-black: #0C0E12; --brand-black: #000000;
--brand-dark-gray: #1E2329; --brand-dark-gray: #0A0A0A;
--brand-light-gray: #EAECEF; --brand-light-gray: #EAECEF;
--brand-almost-white: #FAFAFA; --brand-almost-white: #FAFAFA;
--brand-white: #FFFFFF; --brand-white: #FFFFFF;
@@ -20,14 +25,14 @@
--binance-yellow-light: #FCD535; --binance-yellow-light: #FCD535;
--binance-yellow-glow: rgba(240, 185, 11, 0.2); --binance-yellow-glow: rgba(240, 185, 11, 0.2);
--background: #181A20; /* Binance body bg */ --background: #000000; /* Binance body bg */
--header-bg: #0B0E11; /* Binance header bg */ --header-bg: #000000; /* Binance header bg */
--background-elevated: #181A20; --background-elevated: #000000;
--foreground: #EAECEF; --foreground: #EAECEF;
--panel-bg: #1E2329; --panel-bg: #0A0A0A;
--panel-bg-hover: #252930; --panel-bg-hover: #111111;
--panel-border: #2B3139; --panel-border: #1A1A1A;
--panel-border-hover: #474D57; --panel-border-hover: #2A2A2A;
/* Binance Signature Colors */ /* Binance Signature Colors */
--binance-green: #0ECB81; --binance-green: #0ECB81;
@@ -44,7 +49,7 @@
--text-disabled: #474D57; --text-disabled: #474D57;
/* Chart Colors */ /* Chart Colors */
--grid-stroke: #2B3139; --grid-stroke: #1A1A1A;
--axis-tick: #5E6673; --axis-tick: #5E6673;
--ref-line: #474D57; --ref-line: #474D57;

View File

@@ -32,13 +32,20 @@ function getAuthHeaders(): Record<string, string> {
export const api = { export const api = {
// AI交易员管理接口 // AI交易员管理接口
async getTraders(): Promise<TraderInfo[]> { async getTraders(): Promise<TraderInfo[]> {
const res = await fetch(`${API_BASE}/traders`, { const res = await fetch(`${API_BASE}/my-traders`, {
headers: getAuthHeaders(), headers: getAuthHeaders(),
}); });
if (!res.ok) throw new Error('获取trader列表失败'); if (!res.ok) throw new Error('获取trader列表失败');
return res.json(); return res.json();
}, },
// 获取公开的交易员列表(无需认证)
async getPublicTraders(): Promise<any[]> {
const res = await fetch(`${API_BASE}/traders`);
if (!res.ok) throw new Error('获取公开trader列表失败');
return res.json();
},
async createTrader(request: CreateTraderRequest): Promise<TraderInfo> { async createTrader(request: CreateTraderRequest): Promise<TraderInfo> {
const res = await fetch(`${API_BASE}/traders`, { const res = await fetch(`${API_BASE}/traders`, {
method: 'POST', method: 'POST',
@@ -240,6 +247,33 @@ export const api = {
return res.json(); return res.json();
}, },
// 批量获取多个交易员的历史数据(无需认证)
async getEquityHistoryBatch(traderIds: string[]): Promise<any> {
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<any[]> {
const res = await fetch(`${API_BASE}/top-traders`);
if (!res.ok) throw new Error('获取前5名交易员失败');
return res.json();
},
// 获取公开交易员配置(无需认证)
async getPublicTraderConfig(traderId: string): Promise<any> {
const res = await fetch(`${API_BASE}/trader/${traderId}/config`);
if (!res.ok) throw new Error('获取公开交易员配置失败');
return res.json();
},
// 获取AI学习表现分析支持trader_id // 获取AI学习表现分析支持trader_id
async getPerformance(traderId?: string): Promise<any> { async getPerformance(traderId?: string): Promise<any> {
const url = traderId const url = traderId
@@ -252,11 +286,9 @@ export const api = {
return res.json(); return res.json();
}, },
// 获取竞赛数据 // 获取竞赛数据(无需认证)
async getCompetition(): Promise<CompetitionData> { async getCompetition(): Promise<CompetitionData> {
const res = await fetch(`${API_BASE}/competition`, { const res = await fetch(`${API_BASE}/competition`);
headers: getAuthHeaders(),
});
if (!res.ok) throw new Error('获取竞赛数据失败'); if (!res.ok) throw new Error('获取竞赛数据失败');
return res.json(); return res.json();
}, },

View File

@@ -1,5 +1,6 @@
export interface SystemConfig { export interface SystemConfig {
admin_mode: boolean; admin_mode: boolean;
beta_mode: boolean;
} }
let configPromise: Promise<SystemConfig> | null = null; let configPromise: Promise<SystemConfig> | null = null;

View File

@@ -10,43 +10,71 @@ import CommunitySection from '../components/landing/CommunitySection'
import AnimatedSection from '../components/landing/AnimatedSection' import AnimatedSection from '../components/landing/AnimatedSection'
import LoginModal from '../components/landing/LoginModal' import LoginModal from '../components/landing/LoginModal'
import FooterSection from '../components/landing/FooterSection' import FooterSection from '../components/landing/FooterSection'
import { useAuth } from '../contexts/AuthContext'
import { useLanguage } from '../contexts/LanguageContext'
import { t } from '../i18n/translations'
export function LandingPage() { export function LandingPage() {
const [showLoginModal, setShowLoginModal] = useState(false) const [showLoginModal, setShowLoginModal] = useState(false)
const { user, logout } = useAuth()
const { language, setLanguage } = useLanguage()
const isLoggedIn = !!user
console.log('LandingPage - user:', user, 'isLoggedIn:', isLoggedIn);
return ( return (
<div className='min-h-screen overflow-hidden' style={{ background: 'var(--brand-black)', color: 'var(--brand-light-gray)' }}> <>
<HeaderBar onLoginClick={() => setShowLoginModal(true)} /> <HeaderBar
<HeroSection /> onLoginClick={() => setShowLoginModal(true)}
<AboutSection /> isLoggedIn={isLoggedIn}
<FeaturesSection /> isHomePage={true}
<HowItWorksSection /> 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';
}
}}
/>
<div className='min-h-screen px-4 sm:px-6 lg:px-8' style={{ background: 'var(--brand-black)', color: 'var(--brand-light-gray)' }}>
<HeroSection language={language} />
<AboutSection language={language} />
<FeaturesSection language={language} />
<HowItWorksSection language={language} />
<CommunitySection /> <CommunitySection />
{/* CTA */} {/* CTA */}
<AnimatedSection backgroundColor='var(--panel-bg)'> <AnimatedSection backgroundColor='var(--panel-bg)'>
<div className='max-w-4xl mx-auto text-center'> <div className='max-w-4xl mx-auto text-center'>
<motion.h2 className='text-5xl font-bold mb-6' style={{ color: 'var(--brand-light-gray)' }} initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }}> <motion.h2 className='text-5xl font-bold mb-6' style={{ color: 'var(--brand-light-gray)' }} initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }}>
AI {t('readyToDefine', language)}
</motion.h2> </motion.h2>
<motion.p className='text-xl mb-12' style={{ color: 'var(--text-secondary)' }} initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ delay: 0.1 }}> <motion.p className='text-xl mb-12' style={{ color: 'var(--text-secondary)' }} initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ delay: 0.1 }}>
TradFiNOFX AgentFi {t('startWithCrypto', language)}
</motion.p> </motion.p>
<div className='flex flex-wrap justify-center gap-4'> <div className='flex flex-wrap justify-center gap-4'>
<motion.button onClick={() => 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 }}> <motion.button onClick={() => 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)}
<motion.div animate={{ x: [0, 5, 0] }} transition={{ duration: 1.5, repeat: Infinity }}> <motion.div animate={{ x: [0, 5, 0] }} transition={{ duration: 1.5, repeat: Infinity }}>
<ArrowRight className='w-5 h-5' /> <ArrowRight className='w-5 h-5' />
</motion.div> </motion.div>
</motion.button> </motion.button>
<motion.a href='https://github.com/tinkle-community/nofx/tree/dev' target='_blank' rel='noopener noreferrer' className='flex items-center gap-2 px-10 py-4 rounded-lg font-semibold text-lg' style={{ background: 'var(--brand-dark-gray)', color: 'var(--brand-light-gray)', border: '1px solid rgba(240, 185, 11, 0.2)' }} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}> <motion.a href='https://github.com/tinkle-community/nofx/tree/dev' target='_blank' rel='noopener noreferrer' className='flex items-center gap-2 px-10 py-4 rounded-lg font-semibold text-lg' style={{ background: 'transparent', color: 'var(--brand-light-gray)', border: '2px solid var(--brand-yellow)' }} whileHover={{ scale: 1.05, backgroundColor: 'rgba(240, 185, 11, 0.1)' }} whileTap={{ scale: 0.95 }}>
{t('viewSourceCode', language)}
</motion.a> </motion.a>
</div> </div>
</div> </div>
</AnimatedSection> </AnimatedSection>
{showLoginModal && <LoginModal onClose={() => setShowLoginModal(false)} />} {showLoginModal && <LoginModal onClose={() => setShowLoginModal(false)} language={language} />}
<FooterSection /> <FooterSection language={language} />
</div> </div>
</>
) )
} }

View File

@@ -125,6 +125,7 @@ export interface CreateTraderRequest {
ai_model_id: string; ai_model_id: string;
exchange_id: string; exchange_id: string;
initial_balance: number; initial_balance: number;
scan_interval_minutes?: number;
btc_eth_leverage?: number; btc_eth_leverage?: number;
altcoin_leverage?: number; altcoin_leverage?: number;
trading_symbols?: string; trading_symbols?: string;
@@ -198,5 +199,6 @@ export interface TraderConfigData {
use_coin_pool: boolean; use_coin_pool: boolean;
use_oi_top: boolean; use_oi_top: boolean;
initial_balance: number; initial_balance: number;
scan_interval_minutes: number;
is_running: boolean; is_running: boolean;
} }