mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-13 07:46:54 +08:00
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:
529
api/server.go
529
api/server.go
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
221
generate_beta_code.sh
Executable 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
6
go.mod
@@ -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
8
go.sum
@@ -32,7 +32,6 @@ github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U
|
|||||||
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
github.com/decred/dcrd/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
60
main.go
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
10
web/package-lock.json
generated
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
web/public/images/hand-bg.png
Normal file
BIN
web/public/images/hand-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 446 KiB |
BIN
web/public/images/hand.png
Normal file
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 |
263
web/src/App.tsx
263
web/src/App.tsx
@@ -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');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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' }}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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/7、高波动性完美测试场),未来扩展到股票、期货、外汇。核心:开放架构、AI
|
{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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 交易的未来标准——一个开放、社区驱动的代理式交易操作系统。支持 Binance、Aster 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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: '注册新账号',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
从加密市场起步,扩展到 TradFi。NOFX 是 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>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user