Merge pull request #359 from Icyoung/beta

Beta Merge dev、Bug fix
This commit is contained in:
Icyoung
2025-11-04 01:50:34 +08:00
committed by GitHub
9 changed files with 195 additions and 382 deletions

View File

@@ -71,20 +71,20 @@ 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)
@@ -122,8 +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)
// 指定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)
@@ -156,16 +154,16 @@ 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
@@ -176,9 +174,9 @@ func (s *Server) handleGetSystemConfig(c *gin.Context) {
betaMode := betaModeStr == "true" betaMode := betaModeStr == "true"
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"admin_mode": auth.IsAdminMode(), "admin_mode": auth.IsAdminMode(),
"beta_mode": betaMode, "beta_mode": betaMode,
"default_coins": defaultCoins, "default_coins": defaultCoins,
"btc_eth_leverage": btcEthLeverage, "btc_eth_leverage": btcEthLeverage,
"altcoin_leverage": altcoinLeverage, "altcoin_leverage": altcoinLeverage,
}) })
@@ -188,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 {
@@ -210,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
} }
@@ -306,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
@@ -336,7 +334,7 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
} }
} }
} }
// 设置系统提示词模板默认值 // 设置系统提示词模板默认值
systemPromptTemplate := "default" systemPromptTemplate := "default"
if req.SystemPromptTemplate != "" { if req.SystemPromptTemplate != "" {
@@ -349,8 +347,8 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
scanIntervalMinutes = 3 // 默认3分钟 scanIntervalMinutes = 3 // 默认3分钟
} }
// 创建交易员配置(数据库实体) // 创建交易员配置(数据库实体)
trader := &config.TraderRecord{ trader := &config.TraderRecord{
ID: traderID, ID: traderID,
UserID: userID, UserID: userID,
Name: req.Name, Name: req.Name,
@@ -367,7 +365,7 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
SystemPromptTemplate: systemPromptTemplate, SystemPromptTemplate: systemPromptTemplate,
IsCrossMargin: isCrossMargin, IsCrossMargin: isCrossMargin,
ScanIntervalMinutes: scanIntervalMinutes, ScanIntervalMinutes: scanIntervalMinutes,
IsRunning: false, IsRunning: false,
} }
// 保存到数据库 // 保存到数据库
@@ -396,24 +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"`
ScanIntervalMinutes int `json:"scan_interval_minutes"` 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"`
CustomPrompt string `json:"custom_prompt"` CustomPrompt string `json:"custom_prompt"`
OverrideBasePrompt bool `json:"override_base_prompt"` OverrideBasePrompt bool `json:"override_base_prompt"`
IsCrossMargin *bool `json:"is_cross_margin"` 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()})
@@ -426,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 {
@@ -434,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
@@ -445,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
@@ -462,8 +460,8 @@ func (s *Server) handleUpdateTrader(c *gin.Context) {
scanIntervalMinutes = existingTrader.ScanIntervalMinutes // 保持原值 scanIntervalMinutes = existingTrader.ScanIntervalMinutes // 保持原值
} }
// 更新交易员配置 // 更新交易员配置
trader := &config.TraderRecord{ trader := &config.TraderRecord{
ID: traderID, ID: traderID,
UserID: userID, UserID: userID,
Name: req.Name, Name: req.Name,
@@ -508,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()
@@ -524,7 +522,7 @@ 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": "交易员已删除"})
} }
@@ -546,14 +544,14 @@ func (s *Server) handleStartTrader(c *gin.Context) {
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())
@@ -561,13 +559,13 @@ func (s *Server) handleStartTrader(c *gin.Context) {
log.Printf("❌ 交易员 %s 运行错误: %v", trader.GetName(), err) log.Printf("❌ 交易员 %s 运行错误: %v", trader.GetName(), err)
} }
}() }()
// 更新数据库中的运行状态 // 更新数据库中的运行状态
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": "交易员已启动"})
} }
@@ -589,23 +587,23 @@ func (s *Server) handleStopTrader(c *gin.Context) {
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()
// 更新数据库中的运行状态 // 更新数据库中的运行状态
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": "交易员已停止"})
} }
@@ -614,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 {
@@ -639,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已更新"})
} }
@@ -654,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)
} }
@@ -698,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)
} }
@@ -743,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,
@@ -757,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": "用户信号源配置已保存"})
} }
@@ -844,21 +842,21 @@ func (s *Server) handleGetTraderConfig(c *gin.Context) {
aiModelID := traderConfig.AIModelID aiModelID := traderConfig.AIModelID
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,
"scan_interval_minutes": traderConfig.ScanIntervalMinutes, "scan_interval_minutes": traderConfig.ScanIntervalMinutes,
"btc_eth_leverage": traderConfig.BTCETHLeverage, "btc_eth_leverage": traderConfig.BTCETHLeverage,
"altcoin_leverage": traderConfig.AltcoinLeverage, "altcoin_leverage": traderConfig.AltcoinLeverage,
"trading_symbols": traderConfig.TradingSymbols, "trading_symbols": traderConfig.TradingSymbols,
"custom_prompt": traderConfig.CustomPrompt, "custom_prompt": traderConfig.CustomPrompt,
"override_base_prompt": traderConfig.OverrideBasePrompt, "override_base_prompt": traderConfig.OverrideBasePrompt,
"is_cross_margin": traderConfig.IsCrossMargin, "is_cross_margin": traderConfig.IsCrossMargin,
"use_coin_pool": traderConfig.UseCoinPool, "use_coin_pool": traderConfig.UseCoinPool,
"use_oi_top": traderConfig.UseOITop, "use_oi_top": traderConfig.UseOITop,
"is_running": isRunning, "is_running": isRunning,
} }
c.JSON(http.StatusOK, result) c.JSON(http.StatusOK, result)
@@ -1025,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{
@@ -1039,7 +1037,7 @@ func (s *Server) handleCompetition(c *gin.Context) {
}) })
return return
} }
c.JSON(http.StatusOK, competition) c.JSON(http.StatusOK, competition)
} }
@@ -1166,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头"})
@@ -1283,11 +1281,11 @@ func (s *Server) handleRegister(c *gin.Context) {
// 返回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",
}) })
} }
@@ -1372,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
@@ -1381,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,
}) })
} }
@@ -1445,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)
} }
@@ -1458,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)
} }
@@ -1498,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 {
@@ -1506,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,
}) })
@@ -1515,13 +1513,13 @@ 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,

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

@@ -97,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 {
@@ -170,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
@@ -199,7 +199,7 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel
} }
} }
} }
// 如果没有指定交易币种,使用默认币种 // 如果没有指定交易币种,使用默认币种
if len(tradingCoins) == 0 { if len(tradingCoins) == 0 {
tradingCoins = defaultCoins tradingCoins = defaultCoins
@@ -213,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作为模型标识
@@ -266,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)
@@ -306,7 +306,7 @@ func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModel
} }
} }
} }
// 如果没有指定交易币种,使用默认币种 // 如果没有指定交易币种,使用默认币种
if len(tradingCoins) == 0 { if len(tradingCoins) == 0 {
tradingCoins = defaultCoins tradingCoins = defaultCoins
@@ -372,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)
@@ -855,7 +855,7 @@ func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiMode
} }
} }
} }
// 如果没有指定交易币种,使用默认币种 // 如果没有指定交易币种,使用默认币种
if len(tradingCoins) == 0 { if len(tradingCoins) == 0 {
tradingCoins = defaultCoins tradingCoins = defaultCoins
@@ -870,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密钥
@@ -916,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

@@ -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"
}, },

View File

@@ -13,7 +13,6 @@ 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,
@@ -270,229 +269,35 @@ function App() {
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-3 md:px-6 py-4"> currentPage={currentPage}
{/* Desktop Layout */} language={language}
<div className="hidden md:flex relative items-center"> onLanguageChange={setLanguage}
{/* Left - Logo and Title */} user={user}
<div className="flex items-center gap-3"> onLogout={logout}
<div className="w-8 h-8 flex items-center justify-center"> isAdminMode={systemConfig?.admin_mode}
<img src="/icons/nofx.svg?v=2" alt="NOFX" className="w-8 h-8" /> onPageChange={(page) => {
</div> console.log('Main app onPageChange called with:', page);
<div>
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}> if (page === 'competition') {
{t('appTitle', language)} window.history.pushState({}, '', '/competition');
</h1> setRoute('/competition');
<p className="text-xs mono" style={{ color: '#848E9C' }}> setCurrentPage('competition');
{t('subtitle', language)} } else if (page === 'traders') {
</p> window.history.pushState({}, '', '/traders');
</div> setRoute('/traders');
</div> setCurrentPage('traders');
} else if (page === 'trader') {
{/* Center - Page Toggle (absolutely positioned) */} window.history.pushState({}, '', '/dashboard');
<div className="absolute left-1/2 transform -translate-x-1/2 flex gap-1 rounded p-1" style={{ background: '#1E2329' }}> setRoute('/dashboard');
<button setCurrentPage('trader');
onClick={() => setCurrentPage('competition')} }
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>
{/* Mobile Layout */}
<div className="flex md:hidden flex-col gap-3">
{/* Top Row - Logo, Title and Language */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<img src="/icons/nofx.svg?v=2" alt="NOFX" className="w-7 h-7" />
<div>
<h1 className="text-base font-bold" style={{ color: '#EAECEF' }}>
{t('appTitle', language)}
</h1>
<p className="text-xs mono" style={{ color: '#848E9C' }}>
{t('subtitle', language)}
</p>
</div>
</div>
{/* Language Toggle - Right side on mobile */}
<div className="flex gap-1 rounded p-0.5" style={{ background: '#1E2329' }}>
<button
onClick={() => setLanguage('zh')}
className="px-2 py-1 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-2 py-1 rounded text-xs font-semibold transition-all"
style={language === 'en'
? { background: '#F0B90B', color: '#000' }
: { background: 'transparent', color: '#848E9C' }
}
>
EN
</button>
</div>
</div>
{/* Second Row - Page Toggle */}
<div className="flex gap-1 rounded p-1" style={{ background: '#1E2329' }}>
<button
onClick={() => setCurrentPage('competition')}
className={`flex-1 px-2 py-1.5 rounded text-xs font-semibold transition-all`}
style={currentPage === 'competition'
? { background: '#F0B90B', color: '#000' }
: { background: 'transparent', color: '#848E9C' }
}
>
{t('aiCompetition', language)}
</button>
<button
onClick={() => setCurrentPage('traders')}
className={`flex-1 px-2 py-1.5 rounded text-xs font-semibold transition-all`}
style={currentPage === 'traders'
? { background: '#F0B90B', color: '#000' }
: { background: 'transparent', color: '#848E9C' }
}
>
{t('aiTraders', language)}
</button>
<button
onClick={() => setCurrentPage('trader')}
className={`flex-1 px-2 py-1.5 rounded text-xs font-semibold transition-all`}
style={currentPage === 'trader'
? { background: '#F0B90B', color: '#000' }
: { background: 'transparent', color: '#848E9C' }
}
>
{t('tradingPanel', language)}
</button>
</div>
{/* Third Row - User Info and Logout */}
<div className="flex items-center gap-2">
{/* User Info or Admin Mode */}
{!systemConfig?.admin_mode && user && (
<div className="flex-1 flex items-center gap-2 px-2 py-1.5 rounded" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
<div className="w-5 h-5 rounded-full flex items-center justify-center text-xs font-bold" style={{ background: '#F0B90B', color: '#000' }}>
{user.email[0].toUpperCase()}
</div>
<span className="text-xs truncate" style={{ color: '#EAECEF' }}>{user.email}</span>
</div>
)}
{systemConfig?.admin_mode && (
<div className="flex-1 flex items-center gap-2 px-2 py-1.5 rounded" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
<Zap className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="text-xs font-semibold" style={{ color: '#F0B90B' }}>{t('adminMode', language)}</span>
</div>
)}
{/* Logout Button */}
{!systemConfig?.admin_mode && (
<button
onClick={logout}
className="px-3 py-1.5 rounded text-xs font-semibold transition-all"
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-3 md:px-6 py-4 md: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' ? (