竞赛fix、交易员新增参数

This commit is contained in:
icy
2025-11-01 02:17:11 +08:00
parent 67f8e0f924
commit a7cc5e5ed4
20 changed files with 2065 additions and 585 deletions

View File

@@ -1,12 +1,14 @@
package api
import (
"encoding/json"
"fmt"
"log"
"net/http"
"nofx/auth"
"nofx/config"
"nofx/manager"
"strconv"
"strings"
"time"
@@ -87,7 +89,9 @@ func (s *Server) setupRoutes() {
{
// AI交易员管理
protected.GET("/traders", s.handleTraderList)
protected.GET("/traders/:id/config", s.handleGetTraderConfig)
protected.POST("/traders", s.handleCreateTrader)
protected.PUT("/traders/:id", s.handleUpdateTrader)
protected.DELETE("/traders/:id", s.handleDeleteTrader)
protected.POST("/traders/:id/start", s.handleStartTrader)
protected.POST("/traders/:id/stop", s.handleStopTrader)
@@ -101,6 +105,10 @@ func (s *Server) setupRoutes() {
protected.GET("/exchanges", s.handleGetExchangeConfigs)
protected.PUT("/exchanges", s.handleUpdateExchangeConfigs)
// 用户信号源配置
protected.GET("/user/signal-sources", s.handleGetUserSignalSource)
protected.POST("/user/signal-sources", s.handleSaveUserSignalSource)
// 竞赛总览
protected.GET("/competition", s.handleCompetition)
@@ -127,8 +135,36 @@ func (s *Server) handleHealth(c *gin.Context) {
// handleGetSystemConfig 获取系统配置(客户端需要知道的配置)
func (s *Server) handleGetSystemConfig(c *gin.Context) {
// 获取默认币种
defaultCoinsStr, _ := s.database.GetSystemConfig("default_coins")
var defaultCoins []string
if defaultCoinsStr != "" {
json.Unmarshal([]byte(defaultCoinsStr), &defaultCoins)
}
if len(defaultCoins) == 0 {
// 使用硬编码的默认币种
defaultCoins = []string{"BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "HYPEUSDT"}
}
// 获取杠杆配置
btcEthLeverageStr, _ := s.database.GetSystemConfig("btc_eth_leverage")
altcoinLeverageStr, _ := s.database.GetSystemConfig("altcoin_leverage")
btcEthLeverage := 5
if val, err := strconv.Atoi(btcEthLeverageStr); err == nil && val > 0 {
btcEthLeverage = val
}
altcoinLeverage := 5
if val, err := strconv.Atoi(altcoinLeverageStr); err == nil && val > 0 {
altcoinLeverage = val
}
c.JSON(http.StatusOK, gin.H{
"admin_mode": auth.IsAdminMode(),
"default_coins": defaultCoins,
"btc_eth_leverage": btcEthLeverage,
"altcoin_leverage": altcoinLeverage,
})
}
@@ -164,21 +200,27 @@ func (s *Server) getTraderFromQuery(c *gin.Context) (*manager.TraderManager, str
// AI交易员管理相关结构体
type CreateTraderRequest struct {
Name string `json:"name" binding:"required"`
AIModelID string `json:"ai_model_id" binding:"required"`
ExchangeID string `json:"exchange_id" binding:"required"`
InitialBalance float64 `json:"initial_balance"`
CustomPrompt string `json:"custom_prompt"`
Name string `json:"name" binding:"required"`
AIModelID string `json:"ai_model_id" binding:"required"`
ExchangeID string `json:"exchange_id" binding:"required"`
InitialBalance float64 `json:"initial_balance"`
BTCETHLeverage int `json:"btc_eth_leverage"`
AltcoinLeverage int `json:"altcoin_leverage"`
TradingSymbols string `json:"trading_symbols"`
CustomPrompt string `json:"custom_prompt"`
OverrideBasePrompt bool `json:"override_base_prompt"`
IsCrossMargin *bool `json:"is_cross_margin"` // 指针类型nil表示使用默认值true
IsCrossMargin *bool `json:"is_cross_margin"` // 指针类型nil表示使用默认值true
UseCoinPool bool `json:"use_coin_pool"`
UseOITop bool `json:"use_oi_top"`
}
type ModelConfig struct {
ID string `json:"id"`
Name string `json:"name"`
Provider string `json:"provider"`
Enabled bool `json:"enabled"`
APIKey string `json:"apiKey,omitempty"`
ID string `json:"id"`
Name string `json:"name"`
Provider string `json:"provider"`
Enabled bool `json:"enabled"`
APIKey string `json:"apiKey,omitempty"`
CustomAPIURL string `json:"customApiUrl,omitempty"`
}
type ExchangeConfig struct {
@@ -193,8 +235,9 @@ type ExchangeConfig struct {
type UpdateModelConfigRequest struct {
Models map[string]struct {
Enabled bool `json:"enabled"`
APIKey string `json:"api_key"`
Enabled bool `json:"enabled"`
APIKey string `json:"api_key"`
CustomAPIURL string `json:"custom_api_url"`
} `json:"models"`
}
@@ -220,6 +263,28 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
return
}
// 校验杠杆值
if req.BTCETHLeverage < 0 || req.BTCETHLeverage > 50 {
c.JSON(http.StatusBadRequest, gin.H{"error": "BTC/ETH杠杆必须在1-50倍之间"})
return
}
if req.AltcoinLeverage < 0 || req.AltcoinLeverage > 20 {
c.JSON(http.StatusBadRequest, gin.H{"error": "山寨币杠杆必须在1-20倍之间"})
return
}
// 校验交易币种格式
if req.TradingSymbols != "" {
symbols := strings.Split(req.TradingSymbols, ",")
for _, symbol := range symbols {
symbol = strings.TrimSpace(symbol)
if symbol != "" && !strings.HasSuffix(strings.ToUpper(symbol), "USDT") {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("无效的币种格式: %s必须以USDT结尾", symbol)})
return
}
}
}
// 生成交易员ID
traderID := fmt.Sprintf("%s_%s_%d", req.ExchangeID, req.AIModelID, time.Now().Unix())
@@ -229,6 +294,30 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
isCrossMargin = *req.IsCrossMargin
}
// 设置杠杆默认值(从系统配置获取)
btcEthLeverage := 5
altcoinLeverage := 5
if req.BTCETHLeverage > 0 {
btcEthLeverage = req.BTCETHLeverage
} else {
// 从系统配置获取默认值
if btcEthLeverageStr, _ := s.database.GetSystemConfig("btc_eth_leverage"); btcEthLeverageStr != "" {
if val, err := strconv.Atoi(btcEthLeverageStr); err == nil && val > 0 {
btcEthLeverage = val
}
}
}
if req.AltcoinLeverage > 0 {
altcoinLeverage = req.AltcoinLeverage
} else {
// 从系统配置获取默认值
if altcoinLeverageStr, _ := s.database.GetSystemConfig("altcoin_leverage"); altcoinLeverageStr != "" {
if val, err := strconv.Atoi(altcoinLeverageStr); err == nil && val > 0 {
altcoinLeverage = val
}
}
}
// 创建交易员配置(数据库实体)
trader := &config.TraderRecord{
ID: traderID,
@@ -237,6 +326,11 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
AIModelID: req.AIModelID,
ExchangeID: req.ExchangeID,
InitialBalance: req.InitialBalance,
BTCETHLeverage: btcEthLeverage,
AltcoinLeverage: altcoinLeverage,
TradingSymbols: req.TradingSymbols,
UseCoinPool: req.UseCoinPool,
UseOITop: req.UseOITop,
CustomPrompt: req.CustomPrompt,
OverrideBasePrompt: req.OverrideBasePrompt,
IsCrossMargin: isCrossMargin,
@@ -268,6 +362,108 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
})
}
// UpdateTraderRequest 更新交易员请求
type UpdateTraderRequest struct {
Name string `json:"name" binding:"required"`
AIModelID string `json:"ai_model_id" binding:"required"`
ExchangeID string `json:"exchange_id" binding:"required"`
InitialBalance float64 `json:"initial_balance"`
BTCETHLeverage int `json:"btc_eth_leverage"`
AltcoinLeverage int `json:"altcoin_leverage"`
TradingSymbols string `json:"trading_symbols"`
CustomPrompt string `json:"custom_prompt"`
OverrideBasePrompt bool `json:"override_base_prompt"`
IsCrossMargin *bool `json:"is_cross_margin"`
}
// handleUpdateTrader 更新交易员配置
func (s *Server) handleUpdateTrader(c *gin.Context) {
userID := c.GetString("user_id")
traderID := c.Param("id")
var req UpdateTraderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 检查交易员是否存在且属于当前用户
traders, err := s.database.GetTraders(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取交易员列表失败"})
return
}
var existingTrader *config.TraderRecord
for _, trader := range traders {
if trader.ID == traderID {
existingTrader = trader
break
}
}
if existingTrader == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
return
}
// 设置默认值
isCrossMargin := existingTrader.IsCrossMargin // 保持原值
if req.IsCrossMargin != nil {
isCrossMargin = *req.IsCrossMargin
}
// 设置杠杆默认值
btcEthLeverage := req.BTCETHLeverage
altcoinLeverage := req.AltcoinLeverage
if btcEthLeverage <= 0 {
btcEthLeverage = existingTrader.BTCETHLeverage // 保持原值
}
if altcoinLeverage <= 0 {
altcoinLeverage = existingTrader.AltcoinLeverage // 保持原值
}
// 更新交易员配置
trader := &config.TraderRecord{
ID: traderID,
UserID: userID,
Name: req.Name,
AIModelID: req.AIModelID,
ExchangeID: req.ExchangeID,
InitialBalance: req.InitialBalance,
BTCETHLeverage: btcEthLeverage,
AltcoinLeverage: altcoinLeverage,
TradingSymbols: req.TradingSymbols,
CustomPrompt: req.CustomPrompt,
OverrideBasePrompt: req.OverrideBasePrompt,
IsCrossMargin: isCrossMargin,
ScanIntervalMinutes: existingTrader.ScanIntervalMinutes, // 保持原值
IsRunning: existingTrader.IsRunning, // 保持原值
}
// 更新数据库
err = s.database.UpdateTrader(trader)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新交易员失败: %v", err)})
return
}
// 重新加载交易员到内存
err = s.traderManager.LoadUserTraders(s.database, userID)
if err != nil {
log.Printf("⚠️ 重新加载用户交易员到内存失败: %v", err)
}
log.Printf("✓ 更新交易员成功: %s (模型: %s, 交易所: %s)", req.Name, req.AIModelID, req.ExchangeID)
c.JSON(http.StatusOK, gin.H{
"trader_id": traderID,
"trader_name": req.Name,
"ai_model": req.AIModelID,
"message": "交易员更新成功",
})
}
// handleDeleteTrader 删除交易员
func (s *Server) handleDeleteTrader(c *gin.Context) {
userID := c.GetString("user_id")
@@ -419,7 +615,7 @@ func (s *Server) handleUpdateModelConfigs(c *gin.Context) {
// 更新每个模型的配置
for modelID, modelData := range req.Models {
err := s.database.UpdateAIModel(userID, modelID, modelData.Enabled, modelData.APIKey)
err := s.database.UpdateAIModel(userID, modelID, modelData.Enabled, modelData.APIKey, modelData.CustomAPIURL)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新模型 %s 失败: %v", modelID, err)})
return
@@ -467,6 +663,48 @@ func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "交易所配置已更新"})
}
// handleGetUserSignalSource 获取用户信号源配置
func (s *Server) handleGetUserSignalSource(c *gin.Context) {
userID := c.GetString("user_id")
source, err := s.database.GetUserSignalSource(userID)
if err != nil {
// 如果配置不存在返回空配置而不是404错误
c.JSON(http.StatusOK, gin.H{
"coin_pool_url": "",
"oi_top_url": "",
})
return
}
c.JSON(http.StatusOK, gin.H{
"coin_pool_url": source.CoinPoolURL,
"oi_top_url": source.OITopURL,
})
}
// handleSaveUserSignalSource 保存用户信号源配置
func (s *Server) handleSaveUserSignalSource(c *gin.Context) {
userID := c.GetString("user_id")
var req struct {
CoinPoolURL string `json:"coin_pool_url"`
OITopURL string `json:"oi_top_url"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err := s.database.CreateUserSignalSource(userID, req.CoinPoolURL, req.OITopURL)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("保存用户信号源配置失败: %v", err)})
return
}
log.Printf("✓ 用户信号源配置已保存: user=%s, coin_pool=%s, oi_top=%s", userID, req.CoinPoolURL, req.OITopURL)
c.JSON(http.StatusOK, gin.H{"message": "用户信号源配置已保存"})
}
// handleTraderList trader列表
func (s *Server) handleTraderList(c *gin.Context) {
userID := c.GetString("user_id")
@@ -500,6 +738,51 @@ func (s *Server) handleTraderList(c *gin.Context) {
c.JSON(http.StatusOK, result)
}
// handleGetTraderConfig 获取交易员详细配置
func (s *Server) handleGetTraderConfig(c *gin.Context) {
userID := c.GetString("user_id")
traderID := c.Param("id")
if traderID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "交易员ID不能为空"})
return
}
traderConfig, _, _, err := s.database.GetTraderConfig(userID, traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("获取交易员配置失败: %v", err)})
return
}
// 获取实时运行状态
isRunning := traderConfig.IsRunning
if at, err := s.traderManager.GetTrader(traderID); err == nil {
status := at.GetStatus()
if running, ok := status["is_running"].(bool); ok {
isRunning = running
}
}
result := map[string]interface{}{
"trader_id": traderConfig.ID,
"trader_name": traderConfig.Name,
"ai_model": traderConfig.AIModelID,
"exchange_id": traderConfig.ExchangeID,
"initial_balance": traderConfig.InitialBalance,
"btc_eth_leverage": traderConfig.BTCETHLeverage,
"altcoin_leverage": traderConfig.AltcoinLeverage,
"trading_symbols": traderConfig.TradingSymbols,
"custom_prompt": traderConfig.CustomPrompt,
"override_base_prompt": traderConfig.OverrideBasePrompt,
"is_cross_margin": traderConfig.IsCrossMargin,
"use_coin_pool": traderConfig.UseCoinPool,
"use_oi_top": traderConfig.UseOITop,
"is_running": isRunning,
}
c.JSON(http.StatusOK, result)
}
// handleStatus 系统状态
func (s *Server) handleStatus(c *gin.Context) {
_, traderID, err := s.getTraderFromQuery(c)
@@ -668,7 +951,7 @@ func (s *Server) handleCompetition(c *gin.Context) {
log.Printf("⚠️ 加载用户 %s 的交易员失败: %v", userID, err)
}
competition, err := s.traderManager.GetCompetitionData(userID)
competition, err := s.traderManager.GetCompetitionData()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取竞赛数据失败: %v", err),