mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-16 17:12:25 +08:00
192
api/server.go
192
api/server.go
@@ -71,20 +71,20 @@ func (s *Server) setupRoutes() {
|
|||||||
{
|
{
|
||||||
// 健康检查
|
// 健康检查
|
||||||
api.Any("/health", s.handleHealth)
|
api.Any("/health", s.handleHealth)
|
||||||
|
|
||||||
// 认证相关路由(无需认证)
|
// 认证相关路由(无需认证)
|
||||||
api.POST("/register", s.handleRegister)
|
api.POST("/register", s.handleRegister)
|
||||||
api.POST("/login", s.handleLogin)
|
api.POST("/login", s.handleLogin)
|
||||||
api.POST("/verify-otp", s.handleVerifyOTP)
|
api.POST("/verify-otp", s.handleVerifyOTP)
|
||||||
api.POST("/complete-registration", s.handleCompleteRegistration)
|
api.POST("/complete-registration", s.handleCompleteRegistration)
|
||||||
|
|
||||||
// 系统支持的模型和交易所(无需认证)
|
// 系统支持的模型和交易所(无需认证)
|
||||||
api.GET("/supported-models", s.handleGetSupportedModels)
|
api.GET("/supported-models", s.handleGetSupportedModels)
|
||||||
api.GET("/supported-exchanges", s.handleGetSupportedExchanges)
|
api.GET("/supported-exchanges", s.handleGetSupportedExchanges)
|
||||||
|
|
||||||
// 系统配置(无需认证)
|
// 系统配置(无需认证)
|
||||||
api.GET("/config", s.handleGetSystemConfig)
|
api.GET("/config", s.handleGetSystemConfig)
|
||||||
|
|
||||||
// 系统提示词模板管理(无需认证)
|
// 系统提示词模板管理(无需认证)
|
||||||
api.GET("/prompt-templates", s.handleGetPromptTemplates)
|
api.GET("/prompt-templates", s.handleGetPromptTemplates)
|
||||||
api.GET("/prompt-templates/:name", s.handleGetPromptTemplate)
|
api.GET("/prompt-templates/:name", s.handleGetPromptTemplate)
|
||||||
@@ -122,8 +122,6 @@ func (s *Server) setupRoutes() {
|
|||||||
protected.GET("/user/signal-sources", s.handleGetUserSignalSource)
|
protected.GET("/user/signal-sources", s.handleGetUserSignalSource)
|
||||||
protected.POST("/user/signal-sources", s.handleSaveUserSignalSource)
|
protected.POST("/user/signal-sources", s.handleSaveUserSignalSource)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 指定trader的数据(使用query参数 ?trader_id=xxx)
|
// 指定trader的数据(使用query参数 ?trader_id=xxx)
|
||||||
protected.GET("/status", s.handleStatus)
|
protected.GET("/status", s.handleStatus)
|
||||||
protected.GET("/account", s.handleAccount)
|
protected.GET("/account", s.handleAccount)
|
||||||
@@ -156,16 +154,16 @@ func (s *Server) handleGetSystemConfig(c *gin.Context) {
|
|||||||
// 使用硬编码的默认币种
|
// 使用硬编码的默认币种
|
||||||
defaultCoins = []string{"BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "HYPEUSDT"}
|
defaultCoins = []string{"BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "HYPEUSDT"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取杠杆配置
|
// 获取杠杆配置
|
||||||
btcEthLeverageStr, _ := s.database.GetSystemConfig("btc_eth_leverage")
|
btcEthLeverageStr, _ := s.database.GetSystemConfig("btc_eth_leverage")
|
||||||
altcoinLeverageStr, _ := s.database.GetSystemConfig("altcoin_leverage")
|
altcoinLeverageStr, _ := s.database.GetSystemConfig("altcoin_leverage")
|
||||||
|
|
||||||
btcEthLeverage := 5
|
btcEthLeverage := 5
|
||||||
if val, err := strconv.Atoi(btcEthLeverageStr); err == nil && val > 0 {
|
if val, err := strconv.Atoi(btcEthLeverageStr); err == nil && val > 0 {
|
||||||
btcEthLeverage = val
|
btcEthLeverage = val
|
||||||
}
|
}
|
||||||
|
|
||||||
altcoinLeverage := 5
|
altcoinLeverage := 5
|
||||||
if val, err := strconv.Atoi(altcoinLeverageStr); err == nil && val > 0 {
|
if val, err := strconv.Atoi(altcoinLeverageStr); err == nil && val > 0 {
|
||||||
altcoinLeverage = val
|
altcoinLeverage = val
|
||||||
@@ -176,9 +174,9 @@ func (s *Server) handleGetSystemConfig(c *gin.Context) {
|
|||||||
betaMode := betaModeStr == "true"
|
betaMode := betaModeStr == "true"
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"admin_mode": auth.IsAdminMode(),
|
"admin_mode": auth.IsAdminMode(),
|
||||||
"beta_mode": betaMode,
|
"beta_mode": betaMode,
|
||||||
"default_coins": defaultCoins,
|
"default_coins": defaultCoins,
|
||||||
"btc_eth_leverage": btcEthLeverage,
|
"btc_eth_leverage": btcEthLeverage,
|
||||||
"altcoin_leverage": altcoinLeverage,
|
"altcoin_leverage": altcoinLeverage,
|
||||||
})
|
})
|
||||||
@@ -188,20 +186,20 @@ func (s *Server) handleGetSystemConfig(c *gin.Context) {
|
|||||||
func (s *Server) getTraderFromQuery(c *gin.Context) (*manager.TraderManager, string, error) {
|
func (s *Server) getTraderFromQuery(c *gin.Context) (*manager.TraderManager, string, error) {
|
||||||
userID := c.GetString("user_id")
|
userID := c.GetString("user_id")
|
||||||
traderID := c.Query("trader_id")
|
traderID := c.Query("trader_id")
|
||||||
|
|
||||||
// 确保用户的交易员已加载到内存中
|
// 确保用户的交易员已加载到内存中
|
||||||
err := s.traderManager.LoadUserTraders(s.database, userID)
|
err := s.traderManager.LoadUserTraders(s.database, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("⚠️ 加载用户 %s 的交易员失败: %v", userID, err)
|
log.Printf("⚠️ 加载用户 %s 的交易员失败: %v", userID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if traderID == "" {
|
if traderID == "" {
|
||||||
// 如果没有指定trader_id,返回该用户的第一个trader
|
// 如果没有指定trader_id,返回该用户的第一个trader
|
||||||
ids := s.traderManager.GetTraderIDs()
|
ids := s.traderManager.GetTraderIDs()
|
||||||
if len(ids) == 0 {
|
if len(ids) == 0 {
|
||||||
return nil, "", fmt.Errorf("没有可用的trader")
|
return nil, "", fmt.Errorf("没有可用的trader")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取用户的交易员列表,优先返回用户自己的交易员
|
// 获取用户的交易员列表,优先返回用户自己的交易员
|
||||||
userTraders, err := s.database.GetTraders(userID)
|
userTraders, err := s.database.GetTraders(userID)
|
||||||
if err == nil && len(userTraders) > 0 {
|
if err == nil && len(userTraders) > 0 {
|
||||||
@@ -210,7 +208,7 @@ func (s *Server) getTraderFromQuery(c *gin.Context) (*manager.TraderManager, str
|
|||||||
traderID = ids[0]
|
traderID = ids[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.traderManager, traderID, nil
|
return s.traderManager, traderID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,13 +304,13 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
|||||||
|
|
||||||
// 生成交易员ID
|
// 生成交易员ID
|
||||||
traderID := fmt.Sprintf("%s_%s_%d", req.ExchangeID, req.AIModelID, time.Now().Unix())
|
traderID := fmt.Sprintf("%s_%s_%d", req.ExchangeID, req.AIModelID, time.Now().Unix())
|
||||||
|
|
||||||
// 设置默认值
|
// 设置默认值
|
||||||
isCrossMargin := true // 默认为全仓模式
|
isCrossMargin := true // 默认为全仓模式
|
||||||
if req.IsCrossMargin != nil {
|
if req.IsCrossMargin != nil {
|
||||||
isCrossMargin = *req.IsCrossMargin
|
isCrossMargin = *req.IsCrossMargin
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置杠杆默认值(从系统配置获取)
|
// 设置杠杆默认值(从系统配置获取)
|
||||||
btcEthLeverage := 5
|
btcEthLeverage := 5
|
||||||
altcoinLeverage := 5
|
altcoinLeverage := 5
|
||||||
@@ -336,7 +334,7 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置系统提示词模板默认值
|
// 设置系统提示词模板默认值
|
||||||
systemPromptTemplate := "default"
|
systemPromptTemplate := "default"
|
||||||
if req.SystemPromptTemplate != "" {
|
if req.SystemPromptTemplate != "" {
|
||||||
@@ -349,8 +347,8 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
|||||||
scanIntervalMinutes = 3 // 默认3分钟
|
scanIntervalMinutes = 3 // 默认3分钟
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建交易员配置(数据库实体)
|
// 创建交易员配置(数据库实体)
|
||||||
trader := &config.TraderRecord{
|
trader := &config.TraderRecord{
|
||||||
ID: traderID,
|
ID: traderID,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
@@ -367,7 +365,7 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
|||||||
SystemPromptTemplate: systemPromptTemplate,
|
SystemPromptTemplate: systemPromptTemplate,
|
||||||
IsCrossMargin: isCrossMargin,
|
IsCrossMargin: isCrossMargin,
|
||||||
ScanIntervalMinutes: scanIntervalMinutes,
|
ScanIntervalMinutes: scanIntervalMinutes,
|
||||||
IsRunning: false,
|
IsRunning: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存到数据库
|
// 保存到数据库
|
||||||
@@ -396,24 +394,24 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
|||||||
|
|
||||||
// UpdateTraderRequest 更新交易员请求
|
// UpdateTraderRequest 更新交易员请求
|
||||||
type UpdateTraderRequest struct {
|
type UpdateTraderRequest struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
AIModelID string `json:"ai_model_id" binding:"required"`
|
AIModelID string `json:"ai_model_id" binding:"required"`
|
||||||
ExchangeID string `json:"exchange_id" binding:"required"`
|
ExchangeID string `json:"exchange_id" binding:"required"`
|
||||||
InitialBalance float64 `json:"initial_balance"`
|
InitialBalance float64 `json:"initial_balance"`
|
||||||
ScanIntervalMinutes int `json:"scan_interval_minutes"`
|
ScanIntervalMinutes int `json:"scan_interval_minutes"`
|
||||||
BTCETHLeverage int `json:"btc_eth_leverage"`
|
BTCETHLeverage int `json:"btc_eth_leverage"`
|
||||||
AltcoinLeverage int `json:"altcoin_leverage"`
|
AltcoinLeverage int `json:"altcoin_leverage"`
|
||||||
TradingSymbols string `json:"trading_symbols"`
|
TradingSymbols string `json:"trading_symbols"`
|
||||||
CustomPrompt string `json:"custom_prompt"`
|
CustomPrompt string `json:"custom_prompt"`
|
||||||
OverrideBasePrompt bool `json:"override_base_prompt"`
|
OverrideBasePrompt bool `json:"override_base_prompt"`
|
||||||
IsCrossMargin *bool `json:"is_cross_margin"`
|
IsCrossMargin *bool `json:"is_cross_margin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleUpdateTrader 更新交易员配置
|
// handleUpdateTrader 更新交易员配置
|
||||||
func (s *Server) handleUpdateTrader(c *gin.Context) {
|
func (s *Server) handleUpdateTrader(c *gin.Context) {
|
||||||
userID := c.GetString("user_id")
|
userID := c.GetString("user_id")
|
||||||
traderID := c.Param("id")
|
traderID := c.Param("id")
|
||||||
|
|
||||||
var req UpdateTraderRequest
|
var req UpdateTraderRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
@@ -426,7 +424,7 @@ func (s *Server) handleUpdateTrader(c *gin.Context) {
|
|||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取交易员列表失败"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取交易员列表失败"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var existingTrader *config.TraderRecord
|
var existingTrader *config.TraderRecord
|
||||||
for _, trader := range traders {
|
for _, trader := range traders {
|
||||||
if trader.ID == traderID {
|
if trader.ID == traderID {
|
||||||
@@ -434,7 +432,7 @@ func (s *Server) handleUpdateTrader(c *gin.Context) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if existingTrader == nil {
|
if existingTrader == nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
|
||||||
return
|
return
|
||||||
@@ -445,7 +443,7 @@ func (s *Server) handleUpdateTrader(c *gin.Context) {
|
|||||||
if req.IsCrossMargin != nil {
|
if req.IsCrossMargin != nil {
|
||||||
isCrossMargin = *req.IsCrossMargin
|
isCrossMargin = *req.IsCrossMargin
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置杠杆默认值
|
// 设置杠杆默认值
|
||||||
btcEthLeverage := req.BTCETHLeverage
|
btcEthLeverage := req.BTCETHLeverage
|
||||||
altcoinLeverage := req.AltcoinLeverage
|
altcoinLeverage := req.AltcoinLeverage
|
||||||
@@ -462,8 +460,8 @@ func (s *Server) handleUpdateTrader(c *gin.Context) {
|
|||||||
scanIntervalMinutes = existingTrader.ScanIntervalMinutes // 保持原值
|
scanIntervalMinutes = existingTrader.ScanIntervalMinutes // 保持原值
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新交易员配置
|
// 更新交易员配置
|
||||||
trader := &config.TraderRecord{
|
trader := &config.TraderRecord{
|
||||||
ID: traderID,
|
ID: traderID,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
@@ -508,14 +506,14 @@ func (s *Server) handleUpdateTrader(c *gin.Context) {
|
|||||||
func (s *Server) handleDeleteTrader(c *gin.Context) {
|
func (s *Server) handleDeleteTrader(c *gin.Context) {
|
||||||
userID := c.GetString("user_id")
|
userID := c.GetString("user_id")
|
||||||
traderID := c.Param("id")
|
traderID := c.Param("id")
|
||||||
|
|
||||||
// 从数据库删除
|
// 从数据库删除
|
||||||
err := s.database.DeleteTrader(userID, traderID)
|
err := s.database.DeleteTrader(userID, traderID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("删除交易员失败: %v", err)})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("删除交易员失败: %v", err)})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果交易员正在运行,先停止它
|
// 如果交易员正在运行,先停止它
|
||||||
if trader, err := s.traderManager.GetTrader(traderID); err == nil {
|
if trader, err := s.traderManager.GetTrader(traderID); err == nil {
|
||||||
status := trader.GetStatus()
|
status := trader.GetStatus()
|
||||||
@@ -524,7 +522,7 @@ func (s *Server) handleDeleteTrader(c *gin.Context) {
|
|||||||
log.Printf("⏹ 已停止运行中的交易员: %s", traderID)
|
log.Printf("⏹ 已停止运行中的交易员: %s", traderID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("✓ 交易员已删除: %s", traderID)
|
log.Printf("✓ 交易员已删除: %s", traderID)
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "交易员已删除"})
|
c.JSON(http.StatusOK, gin.H{"message": "交易员已删除"})
|
||||||
}
|
}
|
||||||
@@ -546,14 +544,14 @@ func (s *Server) handleStartTrader(c *gin.Context) {
|
|||||||
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查交易员是否已经在运行
|
// 检查交易员是否已经在运行
|
||||||
status := trader.GetStatus()
|
status := trader.GetStatus()
|
||||||
if isRunning, ok := status["is_running"].(bool); ok && isRunning {
|
if isRunning, ok := status["is_running"].(bool); ok && isRunning {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "交易员已在运行中"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "交易员已在运行中"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动交易员
|
// 启动交易员
|
||||||
go func() {
|
go func() {
|
||||||
log.Printf("▶️ 启动交易员 %s (%s)", traderID, trader.GetName())
|
log.Printf("▶️ 启动交易员 %s (%s)", traderID, trader.GetName())
|
||||||
@@ -561,13 +559,13 @@ func (s *Server) handleStartTrader(c *gin.Context) {
|
|||||||
log.Printf("❌ 交易员 %s 运行错误: %v", trader.GetName(), err)
|
log.Printf("❌ 交易员 %s 运行错误: %v", trader.GetName(), err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// 更新数据库中的运行状态
|
// 更新数据库中的运行状态
|
||||||
err = s.database.UpdateTraderStatus(userID, traderID, true)
|
err = s.database.UpdateTraderStatus(userID, traderID, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("⚠️ 更新交易员状态失败: %v", err)
|
log.Printf("⚠️ 更新交易员状态失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("✓ 交易员 %s 已启动", trader.GetName())
|
log.Printf("✓ 交易员 %s 已启动", trader.GetName())
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "交易员已启动"})
|
c.JSON(http.StatusOK, gin.H{"message": "交易员已启动"})
|
||||||
}
|
}
|
||||||
@@ -589,23 +587,23 @@ func (s *Server) handleStopTrader(c *gin.Context) {
|
|||||||
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查交易员是否正在运行
|
// 检查交易员是否正在运行
|
||||||
status := trader.GetStatus()
|
status := trader.GetStatus()
|
||||||
if isRunning, ok := status["is_running"].(bool); ok && !isRunning {
|
if isRunning, ok := status["is_running"].(bool); ok && !isRunning {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "交易员已停止"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "交易员已停止"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止交易员
|
// 停止交易员
|
||||||
trader.Stop()
|
trader.Stop()
|
||||||
|
|
||||||
// 更新数据库中的运行状态
|
// 更新数据库中的运行状态
|
||||||
err = s.database.UpdateTraderStatus(userID, traderID, false)
|
err = s.database.UpdateTraderStatus(userID, traderID, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("⚠️ 更新交易员状态失败: %v", err)
|
log.Printf("⚠️ 更新交易员状态失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("⏹ 交易员 %s 已停止", trader.GetName())
|
log.Printf("⏹ 交易员 %s 已停止", trader.GetName())
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "交易员已停止"})
|
c.JSON(http.StatusOK, gin.H{"message": "交易员已停止"})
|
||||||
}
|
}
|
||||||
@@ -614,24 +612,24 @@ func (s *Server) handleStopTrader(c *gin.Context) {
|
|||||||
func (s *Server) handleUpdateTraderPrompt(c *gin.Context) {
|
func (s *Server) handleUpdateTraderPrompt(c *gin.Context) {
|
||||||
traderID := c.Param("id")
|
traderID := c.Param("id")
|
||||||
userID := c.GetString("user_id")
|
userID := c.GetString("user_id")
|
||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
CustomPrompt string `json:"custom_prompt"`
|
CustomPrompt string `json:"custom_prompt"`
|
||||||
OverrideBasePrompt bool `json:"override_base_prompt"`
|
OverrideBasePrompt bool `json:"override_base_prompt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新数据库
|
// 更新数据库
|
||||||
err := s.database.UpdateTraderCustomPrompt(userID, traderID, req.CustomPrompt, req.OverrideBasePrompt)
|
err := s.database.UpdateTraderCustomPrompt(userID, traderID, req.CustomPrompt, req.OverrideBasePrompt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新自定义prompt失败: %v", err)})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新自定义prompt失败: %v", err)})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果trader在内存中,更新其custom prompt和override设置
|
// 如果trader在内存中,更新其custom prompt和override设置
|
||||||
trader, err := s.traderManager.GetTrader(traderID)
|
trader, err := s.traderManager.GetTrader(traderID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -639,7 +637,7 @@ func (s *Server) handleUpdateTraderPrompt(c *gin.Context) {
|
|||||||
trader.SetOverrideBasePrompt(req.OverrideBasePrompt)
|
trader.SetOverrideBasePrompt(req.OverrideBasePrompt)
|
||||||
log.Printf("✓ 已更新交易员 %s 的自定义prompt (覆盖基础=%v)", trader.GetName(), req.OverrideBasePrompt)
|
log.Printf("✓ 已更新交易员 %s 的自定义prompt (覆盖基础=%v)", trader.GetName(), req.OverrideBasePrompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "自定义prompt已更新"})
|
c.JSON(http.StatusOK, gin.H{"message": "自定义prompt已更新"})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,7 +652,7 @@ func (s *Server) handleGetModelConfigs(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("✅ 找到 %d 个AI模型配置", len(models))
|
log.Printf("✅ 找到 %d 个AI模型配置", len(models))
|
||||||
|
|
||||||
c.JSON(http.StatusOK, models)
|
c.JSON(http.StatusOK, models)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -698,7 +696,7 @@ func (s *Server) handleGetExchangeConfigs(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("✅ 找到 %d 个交易所配置", len(exchanges))
|
log.Printf("✅ 找到 %d 个交易所配置", len(exchanges))
|
||||||
|
|
||||||
c.JSON(http.StatusOK, exchanges)
|
c.JSON(http.StatusOK, exchanges)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -743,7 +741,7 @@ func (s *Server) handleGetUserSignalSource(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"coin_pool_url": source.CoinPoolURL,
|
"coin_pool_url": source.CoinPoolURL,
|
||||||
"oi_top_url": source.OITopURL,
|
"oi_top_url": source.OITopURL,
|
||||||
@@ -757,18 +755,18 @@ func (s *Server) handleSaveUserSignalSource(c *gin.Context) {
|
|||||||
CoinPoolURL string `json:"coin_pool_url"`
|
CoinPoolURL string `json:"coin_pool_url"`
|
||||||
OITopURL string `json:"oi_top_url"`
|
OITopURL string `json:"oi_top_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.database.CreateUserSignalSource(userID, req.CoinPoolURL, req.OITopURL)
|
err := s.database.CreateUserSignalSource(userID, req.CoinPoolURL, req.OITopURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("保存用户信号源配置失败: %v", err)})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("保存用户信号源配置失败: %v", err)})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("✓ 用户信号源配置已保存: user=%s, coin_pool=%s, oi_top=%s", userID, req.CoinPoolURL, req.OITopURL)
|
log.Printf("✓ 用户信号源配置已保存: user=%s, coin_pool=%s, oi_top=%s", userID, req.CoinPoolURL, req.OITopURL)
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "用户信号源配置已保存"})
|
c.JSON(http.StatusOK, gin.H{"message": "用户信号源配置已保存"})
|
||||||
}
|
}
|
||||||
@@ -844,21 +842,21 @@ func (s *Server) handleGetTraderConfig(c *gin.Context) {
|
|||||||
aiModelID := traderConfig.AIModelID
|
aiModelID := traderConfig.AIModelID
|
||||||
|
|
||||||
result := map[string]interface{}{
|
result := map[string]interface{}{
|
||||||
"trader_id": traderConfig.ID,
|
"trader_id": traderConfig.ID,
|
||||||
"trader_name": traderConfig.Name,
|
"trader_name": traderConfig.Name,
|
||||||
"ai_model": aiModelID,
|
"ai_model": aiModelID,
|
||||||
"exchange_id": traderConfig.ExchangeID,
|
"exchange_id": traderConfig.ExchangeID,
|
||||||
"initial_balance": traderConfig.InitialBalance,
|
"initial_balance": traderConfig.InitialBalance,
|
||||||
"scan_interval_minutes": traderConfig.ScanIntervalMinutes,
|
"scan_interval_minutes": traderConfig.ScanIntervalMinutes,
|
||||||
"btc_eth_leverage": traderConfig.BTCETHLeverage,
|
"btc_eth_leverage": traderConfig.BTCETHLeverage,
|
||||||
"altcoin_leverage": traderConfig.AltcoinLeverage,
|
"altcoin_leverage": traderConfig.AltcoinLeverage,
|
||||||
"trading_symbols": traderConfig.TradingSymbols,
|
"trading_symbols": traderConfig.TradingSymbols,
|
||||||
"custom_prompt": traderConfig.CustomPrompt,
|
"custom_prompt": traderConfig.CustomPrompt,
|
||||||
"override_base_prompt": traderConfig.OverrideBasePrompt,
|
"override_base_prompt": traderConfig.OverrideBasePrompt,
|
||||||
"is_cross_margin": traderConfig.IsCrossMargin,
|
"is_cross_margin": traderConfig.IsCrossMargin,
|
||||||
"use_coin_pool": traderConfig.UseCoinPool,
|
"use_coin_pool": traderConfig.UseCoinPool,
|
||||||
"use_oi_top": traderConfig.UseOITop,
|
"use_oi_top": traderConfig.UseOITop,
|
||||||
"is_running": isRunning,
|
"is_running": isRunning,
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, result)
|
c.JSON(http.StatusOK, result)
|
||||||
@@ -1025,13 +1023,13 @@ func (s *Server) handleStatistics(c *gin.Context) {
|
|||||||
// handleCompetition 竞赛总览(对比所有trader)
|
// handleCompetition 竞赛总览(对比所有trader)
|
||||||
func (s *Server) handleCompetition(c *gin.Context) {
|
func (s *Server) handleCompetition(c *gin.Context) {
|
||||||
userID := c.GetString("user_id")
|
userID := c.GetString("user_id")
|
||||||
|
|
||||||
// 确保用户的交易员已加载到内存中
|
// 确保用户的交易员已加载到内存中
|
||||||
err := s.traderManager.LoadUserTraders(s.database, userID)
|
err := s.traderManager.LoadUserTraders(s.database, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("⚠️ 加载用户 %s 的交易员失败: %v", userID, err)
|
log.Printf("⚠️ 加载用户 %s 的交易员失败: %v", userID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
competition, err := s.traderManager.GetCompetitionData()
|
competition, err := s.traderManager.GetCompetitionData()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
@@ -1039,7 +1037,7 @@ func (s *Server) handleCompetition(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, competition)
|
c.JSON(http.StatusOK, competition)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1166,7 +1164,7 @@ func (s *Server) authMiddleware() gin.HandlerFunc {
|
|||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
authHeader := c.GetHeader("Authorization")
|
authHeader := c.GetHeader("Authorization")
|
||||||
if authHeader == "" {
|
if authHeader == "" {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "缺少Authorization头"})
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "缺少Authorization头"})
|
||||||
@@ -1283,11 +1281,11 @@ func (s *Server) handleRegister(c *gin.Context) {
|
|||||||
// 返回OTP设置信息
|
// 返回OTP设置信息
|
||||||
qrCodeURL := auth.GetOTPQRCodeURL(otpSecret, req.Email)
|
qrCodeURL := auth.GetOTPQRCodeURL(otpSecret, req.Email)
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
"email": req.Email,
|
"email": req.Email,
|
||||||
"otp_secret": otpSecret,
|
"otp_secret": otpSecret,
|
||||||
"qr_code_url": qrCodeURL,
|
"qr_code_url": qrCodeURL,
|
||||||
"message": "请使用Google Authenticator扫描二维码并验证OTP",
|
"message": "请使用Google Authenticator扫描二维码并验证OTP",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1372,8 +1370,8 @@ func (s *Server) handleLogin(c *gin.Context) {
|
|||||||
// 检查OTP是否已验证
|
// 检查OTP是否已验证
|
||||||
if !user.OTPVerified {
|
if !user.OTPVerified {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{
|
c.JSON(http.StatusUnauthorized, gin.H{
|
||||||
"error": "账户未完成OTP设置",
|
"error": "账户未完成OTP设置",
|
||||||
"user_id": user.ID,
|
"user_id": user.ID,
|
||||||
"requires_otp_setup": true,
|
"requires_otp_setup": true,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -1381,9 +1379,9 @@ func (s *Server) handleLogin(c *gin.Context) {
|
|||||||
|
|
||||||
// 返回需要OTP验证的状态
|
// 返回需要OTP验证的状态
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"user_id": user.ID,
|
"user_id": user.ID,
|
||||||
"email": user.Email,
|
"email": user.Email,
|
||||||
"message": "请输入Google Authenticator验证码",
|
"message": "请输入Google Authenticator验证码",
|
||||||
"requires_otp": true,
|
"requires_otp": true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1445,7 +1443,7 @@ func (s *Server) handleGetSupportedModels(c *gin.Context) {
|
|||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取支持的AI模型失败"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取支持的AI模型失败"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, models)
|
c.JSON(http.StatusOK, models)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1458,7 +1456,7 @@ func (s *Server) handleGetSupportedExchanges(c *gin.Context) {
|
|||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取支持的交易所失败"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取支持的交易所失败"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, exchanges)
|
c.JSON(http.StatusOK, exchanges)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1498,7 +1496,7 @@ func (s *Server) Start() error {
|
|||||||
func (s *Server) handleGetPromptTemplates(c *gin.Context) {
|
func (s *Server) handleGetPromptTemplates(c *gin.Context) {
|
||||||
// 导入 decision 包
|
// 导入 decision 包
|
||||||
templates := decision.GetAllPromptTemplates()
|
templates := decision.GetAllPromptTemplates()
|
||||||
|
|
||||||
// 转换为响应格式
|
// 转换为响应格式
|
||||||
response := make([]map[string]interface{}, 0, len(templates))
|
response := make([]map[string]interface{}, 0, len(templates))
|
||||||
for _, tmpl := range templates {
|
for _, tmpl := range templates {
|
||||||
@@ -1506,7 +1504,7 @@ func (s *Server) handleGetPromptTemplates(c *gin.Context) {
|
|||||||
"name": tmpl.Name,
|
"name": tmpl.Name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"templates": response,
|
"templates": response,
|
||||||
})
|
})
|
||||||
@@ -1515,13 +1513,13 @@ func (s *Server) handleGetPromptTemplates(c *gin.Context) {
|
|||||||
// handleGetPromptTemplate 获取指定名称的提示词模板内容
|
// handleGetPromptTemplate 获取指定名称的提示词模板内容
|
||||||
func (s *Server) handleGetPromptTemplate(c *gin.Context) {
|
func (s *Server) handleGetPromptTemplate(c *gin.Context) {
|
||||||
templateName := c.Param("name")
|
templateName := c.Param("name")
|
||||||
|
|
||||||
template, err := decision.GetPromptTemplate(templateName)
|
template, err := decision.GetPromptTemplate(templateName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("模板不存在: %s", templateName)})
|
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("模板不存在: %s", templateName)})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"name": template.Name,
|
"name": template.Name,
|
||||||
"content": template.Content,
|
"content": template.Content,
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 为每个交易员获取AI模型和交易所配置
|
// 为每个交易员获取AI模型和交易所配置
|
||||||
for _, traderCfg := range allTraders {
|
for _, traderCfg := range allTraders {
|
||||||
// 获取AI模型配置(使用交易员所属的用户ID)
|
// 获取AI模型配置(使用交易员所属的用户ID)
|
||||||
aiModels, err := database.GetAIModels(traderCfg.UserID)
|
aiModels, err := database.GetAIModels(traderCfg.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -170,7 +170,7 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 添加到TraderManager
|
// 添加到TraderManager
|
||||||
err = tm.addTraderFromDB(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, oiTopURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, defaultCoins)
|
err = tm.addTraderFromDB(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, oiTopURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, defaultCoins)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("❌ 添加交易员 %s 失败: %v", traderCfg.Name, err)
|
log.Printf("❌ 添加交易员 %s 失败: %v", traderCfg.Name, err)
|
||||||
continue
|
continue
|
||||||
@@ -199,7 +199,7 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有指定交易币种,使用默认币种
|
// 如果没有指定交易币种,使用默认币种
|
||||||
if len(tradingCoins) == 0 {
|
if len(tradingCoins) == 0 {
|
||||||
tradingCoins = defaultCoins
|
tradingCoins = defaultCoins
|
||||||
@@ -213,7 +213,7 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 构建AutoTraderConfig
|
// 构建AutoTraderConfig
|
||||||
traderConfig := trader.AutoTraderConfig{
|
traderConfig := trader.AutoTraderConfig{
|
||||||
ID: traderCfg.ID,
|
ID: traderCfg.ID,
|
||||||
Name: traderCfg.Name,
|
Name: traderCfg.Name,
|
||||||
AIModel: aiModelCfg.Provider, // 使用provider作为模型标识
|
AIModel: aiModelCfg.Provider, // 使用provider作为模型标识
|
||||||
@@ -266,7 +266,7 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("创建trader失败: %w", err)
|
return fmt.Errorf("创建trader失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置自定义prompt(如果有)
|
// 设置自定义prompt(如果有)
|
||||||
if traderCfg.CustomPrompt != "" {
|
if traderCfg.CustomPrompt != "" {
|
||||||
at.SetCustomPrompt(traderCfg.CustomPrompt)
|
at.SetCustomPrompt(traderCfg.CustomPrompt)
|
||||||
@@ -306,7 +306,7 @@ func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有指定交易币种,使用默认币种
|
// 如果没有指定交易币种,使用默认币种
|
||||||
if len(tradingCoins) == 0 {
|
if len(tradingCoins) == 0 {
|
||||||
tradingCoins = defaultCoins
|
tradingCoins = defaultCoins
|
||||||
@@ -372,7 +372,7 @@ func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModel
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("创建trader失败: %w", err)
|
return fmt.Errorf("创建trader失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置自定义prompt(如果有)
|
// 设置自定义prompt(如果有)
|
||||||
if traderCfg.CustomPrompt != "" {
|
if traderCfg.CustomPrompt != "" {
|
||||||
at.SetCustomPrompt(traderCfg.CustomPrompt)
|
at.SetCustomPrompt(traderCfg.CustomPrompt)
|
||||||
@@ -855,7 +855,7 @@ func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiMode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有指定交易币种,使用默认币种
|
// 如果没有指定交易币种,使用默认币种
|
||||||
if len(tradingCoins) == 0 {
|
if len(tradingCoins) == 0 {
|
||||||
tradingCoins = defaultCoins
|
tradingCoins = defaultCoins
|
||||||
@@ -870,25 +870,25 @@ func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiMode
|
|||||||
|
|
||||||
// 构建AutoTraderConfig
|
// 构建AutoTraderConfig
|
||||||
traderConfig := trader.AutoTraderConfig{
|
traderConfig := trader.AutoTraderConfig{
|
||||||
ID: traderCfg.ID,
|
ID: traderCfg.ID,
|
||||||
Name: traderCfg.Name,
|
Name: traderCfg.Name,
|
||||||
AIModel: aiModelCfg.Provider, // 使用provider作为模型标识
|
AIModel: aiModelCfg.Provider, // 使用provider作为模型标识
|
||||||
Exchange: exchangeCfg.ID, // 使用exchange ID
|
Exchange: exchangeCfg.ID, // 使用exchange ID
|
||||||
InitialBalance: traderCfg.InitialBalance,
|
InitialBalance: traderCfg.InitialBalance,
|
||||||
BTCETHLeverage: traderCfg.BTCETHLeverage,
|
BTCETHLeverage: traderCfg.BTCETHLeverage,
|
||||||
AltcoinLeverage: traderCfg.AltcoinLeverage,
|
AltcoinLeverage: traderCfg.AltcoinLeverage,
|
||||||
ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute,
|
ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute,
|
||||||
CoinPoolAPIURL: effectiveCoinPoolURL,
|
CoinPoolAPIURL: effectiveCoinPoolURL,
|
||||||
CustomAPIURL: aiModelCfg.CustomAPIURL, // 自定义API URL
|
CustomAPIURL: aiModelCfg.CustomAPIURL, // 自定义API URL
|
||||||
CustomModelName: aiModelCfg.CustomModelName, // 自定义模型名称
|
CustomModelName: aiModelCfg.CustomModelName, // 自定义模型名称
|
||||||
UseQwen: aiModelCfg.Provider == "qwen",
|
UseQwen: aiModelCfg.Provider == "qwen",
|
||||||
MaxDailyLoss: maxDailyLoss,
|
MaxDailyLoss: maxDailyLoss,
|
||||||
MaxDrawdown: maxDrawdown,
|
MaxDrawdown: maxDrawdown,
|
||||||
StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute,
|
StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute,
|
||||||
IsCrossMargin: traderCfg.IsCrossMargin,
|
IsCrossMargin: traderCfg.IsCrossMargin,
|
||||||
DefaultCoins: defaultCoins,
|
DefaultCoins: defaultCoins,
|
||||||
TradingCoins: tradingCoins,
|
TradingCoins: tradingCoins,
|
||||||
SystemPromptTemplate: traderCfg.SystemPromptTemplate, // 系统提示词模板
|
SystemPromptTemplate: traderCfg.SystemPromptTemplate, // 系统提示词模板
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据交易所类型设置API密钥
|
// 根据交易所类型设置API密钥
|
||||||
@@ -916,7 +916,7 @@ func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiMode
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("创建trader失败: %w", err)
|
return fmt.Errorf("创建trader失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置自定义prompt(如果有)
|
// 设置自定义prompt(如果有)
|
||||||
if traderCfg.CustomPrompt != "" {
|
if traderCfg.CustomPrompt != "" {
|
||||||
at.SetCustomPrompt(traderCfg.CustomPrompt)
|
at.SetCustomPrompt(traderCfg.CustomPrompt)
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
249
web/src/App.tsx
249
web/src/App.tsx
@@ -13,7 +13,6 @@ import { LanguageProvider, useLanguage } from './contexts/LanguageContext';
|
|||||||
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||||
import { t, type Language } from './i18n/translations';
|
import { t, type Language } from './i18n/translations';
|
||||||
import { useSystemConfig } from './hooks/useSystemConfig';
|
import { useSystemConfig } from './hooks/useSystemConfig';
|
||||||
import { Zap } from 'lucide-react';
|
|
||||||
import type {
|
import type {
|
||||||
SystemStatus,
|
SystemStatus,
|
||||||
AccountInfo,
|
AccountInfo,
|
||||||
@@ -270,229 +269,35 @@ function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen" style={{ background: '#0B0E11', color: '#EAECEF' }}>
|
<div className="min-h-screen" style={{ background: '#0B0E11', color: '#EAECEF' }}>
|
||||||
{/* Header - Binance Style */}
|
<HeaderBar
|
||||||
<header className="glass sticky top-0 z-50 backdrop-blur-xl">
|
isLoggedIn={!!user}
|
||||||
<div className="max-w-[1920px] mx-auto px-3 md:px-6 py-4">
|
currentPage={currentPage}
|
||||||
{/* Desktop Layout */}
|
language={language}
|
||||||
<div className="hidden md:flex relative items-center">
|
onLanguageChange={setLanguage}
|
||||||
{/* Left - Logo and Title */}
|
user={user}
|
||||||
<div className="flex items-center gap-3">
|
onLogout={logout}
|
||||||
<div className="w-8 h-8 flex items-center justify-center">
|
isAdminMode={systemConfig?.admin_mode}
|
||||||
<img src="/icons/nofx.svg?v=2" alt="NOFX" className="w-8 h-8" />
|
onPageChange={(page) => {
|
||||||
</div>
|
console.log('Main app onPageChange called with:', page);
|
||||||
<div>
|
|
||||||
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
|
if (page === 'competition') {
|
||||||
{t('appTitle', language)}
|
window.history.pushState({}, '', '/competition');
|
||||||
</h1>
|
setRoute('/competition');
|
||||||
<p className="text-xs mono" style={{ color: '#848E9C' }}>
|
setCurrentPage('competition');
|
||||||
{t('subtitle', language)}
|
} else if (page === 'traders') {
|
||||||
</p>
|
window.history.pushState({}, '', '/traders');
|
||||||
</div>
|
setRoute('/traders');
|
||||||
</div>
|
setCurrentPage('traders');
|
||||||
|
} else if (page === 'trader') {
|
||||||
{/* Center - Page Toggle (absolutely positioned) */}
|
window.history.pushState({}, '', '/dashboard');
|
||||||
<div className="absolute left-1/2 transform -translate-x-1/2 flex gap-1 rounded p-1" style={{ background: '#1E2329' }}>
|
setRoute('/dashboard');
|
||||||
<button
|
setCurrentPage('trader');
|
||||||
onClick={() => setCurrentPage('competition')}
|
}
|
||||||
className={`px-3 py-2 rounded text-sm font-semibold transition-all`}
|
}}
|
||||||
style={currentPage === 'competition'
|
/>
|
||||||
? { background: '#F0B90B', color: '#000' }
|
|
||||||
: { background: 'transparent', color: '#848E9C' }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('aiCompetition', language)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setCurrentPage('traders')}
|
|
||||||
className={`px-3 py-2 rounded text-sm font-semibold transition-all`}
|
|
||||||
style={currentPage === 'traders'
|
|
||||||
? { background: '#F0B90B', color: '#000' }
|
|
||||||
: { background: 'transparent', color: '#848E9C' }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('aiTraders', language)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setCurrentPage('trader')}
|
|
||||||
className={`px-3 py-2 rounded text-sm font-semibold transition-all`}
|
|
||||||
style={currentPage === 'trader'
|
|
||||||
? { background: '#F0B90B', color: '#000' }
|
|
||||||
: { background: 'transparent', color: '#848E9C' }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('tradingPanel', language)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right - Actions */}
|
|
||||||
<div className="ml-auto flex items-center gap-3">
|
|
||||||
|
|
||||||
{/* User Info - Only show if not in admin mode */}
|
|
||||||
{!systemConfig?.admin_mode && user && (
|
|
||||||
<div className="flex items-center gap-2 px-3 py-2 rounded" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
|
||||||
<div className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold" style={{ background: '#F0B90B', color: '#000' }}>
|
|
||||||
{user.email[0].toUpperCase()}
|
|
||||||
</div>
|
|
||||||
<span className="text-sm" style={{ color: '#EAECEF' }}>{user.email}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Admin Mode Indicator */}
|
|
||||||
{systemConfig?.admin_mode && (
|
|
||||||
<div className="flex items-center gap-2 px-3 py-2 rounded" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
|
||||||
<Zap className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
|
||||||
<span className="text-sm font-semibold" style={{ color: '#F0B90B' }}>{t('adminMode', language)}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Language Toggle */}
|
|
||||||
<div className="flex gap-1 rounded p-1" style={{ background: '#1E2329' }}>
|
|
||||||
<button
|
|
||||||
onClick={() => setLanguage('zh')}
|
|
||||||
className="px-3 py-1.5 rounded text-xs font-semibold transition-all"
|
|
||||||
style={language === 'zh'
|
|
||||||
? { background: '#F0B90B', color: '#000' }
|
|
||||||
: { background: 'transparent', color: '#848E9C' }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
中文
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setLanguage('en')}
|
|
||||||
className="px-3 py-1.5 rounded text-xs font-semibold transition-all"
|
|
||||||
style={language === 'en'
|
|
||||||
? { background: '#F0B90B', color: '#000' }
|
|
||||||
: { background: 'transparent', color: '#848E9C' }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
EN
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Logout Button - Only show if not in admin mode */}
|
|
||||||
{!systemConfig?.admin_mode && (
|
|
||||||
<button
|
|
||||||
onClick={logout}
|
|
||||||
className="px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
|
|
||||||
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D', border: '1px solid rgba(246, 70, 93, 0.2)' }}
|
|
||||||
>
|
|
||||||
{t('logout', language)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile Layout */}
|
|
||||||
<div className="flex md:hidden flex-col gap-3">
|
|
||||||
{/* Top Row - Logo, Title and Language */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<img src="/icons/nofx.svg?v=2" alt="NOFX" className="w-7 h-7" />
|
|
||||||
<div>
|
|
||||||
<h1 className="text-base font-bold" style={{ color: '#EAECEF' }}>
|
|
||||||
{t('appTitle', language)}
|
|
||||||
</h1>
|
|
||||||
<p className="text-xs mono" style={{ color: '#848E9C' }}>
|
|
||||||
{t('subtitle', language)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Language Toggle - Right side on mobile */}
|
|
||||||
<div className="flex gap-1 rounded p-0.5" style={{ background: '#1E2329' }}>
|
|
||||||
<button
|
|
||||||
onClick={() => setLanguage('zh')}
|
|
||||||
className="px-2 py-1 rounded text-xs font-semibold transition-all"
|
|
||||||
style={language === 'zh'
|
|
||||||
? { background: '#F0B90B', color: '#000' }
|
|
||||||
: { background: 'transparent', color: '#848E9C' }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
中文
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setLanguage('en')}
|
|
||||||
className="px-2 py-1 rounded text-xs font-semibold transition-all"
|
|
||||||
style={language === 'en'
|
|
||||||
? { background: '#F0B90B', color: '#000' }
|
|
||||||
: { background: 'transparent', color: '#848E9C' }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
EN
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Second Row - Page Toggle */}
|
|
||||||
<div className="flex gap-1 rounded p-1" style={{ background: '#1E2329' }}>
|
|
||||||
<button
|
|
||||||
onClick={() => setCurrentPage('competition')}
|
|
||||||
className={`flex-1 px-2 py-1.5 rounded text-xs font-semibold transition-all`}
|
|
||||||
style={currentPage === 'competition'
|
|
||||||
? { background: '#F0B90B', color: '#000' }
|
|
||||||
: { background: 'transparent', color: '#848E9C' }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('aiCompetition', language)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setCurrentPage('traders')}
|
|
||||||
className={`flex-1 px-2 py-1.5 rounded text-xs font-semibold transition-all`}
|
|
||||||
style={currentPage === 'traders'
|
|
||||||
? { background: '#F0B90B', color: '#000' }
|
|
||||||
: { background: 'transparent', color: '#848E9C' }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('aiTraders', language)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setCurrentPage('trader')}
|
|
||||||
className={`flex-1 px-2 py-1.5 rounded text-xs font-semibold transition-all`}
|
|
||||||
style={currentPage === 'trader'
|
|
||||||
? { background: '#F0B90B', color: '#000' }
|
|
||||||
: { background: 'transparent', color: '#848E9C' }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('tradingPanel', language)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Third Row - User Info and Logout */}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{/* User Info or Admin Mode */}
|
|
||||||
{!systemConfig?.admin_mode && user && (
|
|
||||||
<div className="flex-1 flex items-center gap-2 px-2 py-1.5 rounded" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
|
||||||
<div className="w-5 h-5 rounded-full flex items-center justify-center text-xs font-bold" style={{ background: '#F0B90B', color: '#000' }}>
|
|
||||||
{user.email[0].toUpperCase()}
|
|
||||||
</div>
|
|
||||||
<span className="text-xs truncate" style={{ color: '#EAECEF' }}>{user.email}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{systemConfig?.admin_mode && (
|
|
||||||
<div className="flex-1 flex items-center gap-2 px-2 py-1.5 rounded" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
|
||||||
<Zap className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
|
||||||
<span className="text-xs font-semibold" style={{ color: '#F0B90B' }}>{t('adminMode', language)}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Logout Button */}
|
|
||||||
{!systemConfig?.admin_mode && (
|
|
||||||
<button
|
|
||||||
onClick={logout}
|
|
||||||
className="px-3 py-1.5 rounded text-xs font-semibold transition-all"
|
|
||||||
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D', border: '1px solid rgba(246, 70, 93, 0.2)' }}
|
|
||||||
>
|
|
||||||
{t('logout', language)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="max-w-[1920px] mx-auto px-3 md:px-6 py-4 md:py-6">
|
<main className="max-w-[1920px] mx-auto px-6 py-6 pt-24">
|
||||||
{currentPage === 'competition' ? (
|
{currentPage === 'competition' ? (
|
||||||
<CompetitionPage />
|
<CompetitionPage />
|
||||||
) : currentPage === 'traders' ? (
|
) : currentPage === 'traders' ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user