Files
nofx/api/server.go
tinkle-community 1ea1234c64 Feature/custom strategy (#1172)
* feat: add Strategy Studio with multi-timeframe support

- Add Strategy Studio page with three-column layout for strategy management
- Support multi-timeframe K-line data selection (5m, 15m, 1h, 4h, etc.)
- Add GetWithTimeframes() function in market package for fetching multiple timeframes
- Add TimeframeSeriesData struct for storing per-timeframe technical indicators
- Update formatMarketData() to display all selected timeframes in AI prompt
- Add strategy API endpoints for CRUD operations and test run
- Integrate real AI test runs with configured AI models
- Support custom AI500 and OI Top API URLs from strategy config

* docs: add Strategy Studio screenshot to README files

* fix: correct strategy-studio.png filename case in README

* refactor: remove legacy signal source config and simplify trader creation

- Remove signal source configuration from traders page (now handled by strategy)
- Remove advanced options (legacy config) from TraderConfigModal
- Rename default strategy to "默认山寨策略" with AI500 coin pool URL
- Delete SignalSourceModal and SignalSourceWarning components
- Clean up related stores, hooks, and page components
2025-12-06 07:20:11 +08:00

2345 lines
74 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package api
import (
"context"
"encoding/json"
"fmt"
"nofx/logger"
"net"
"net/http"
"nofx/auth"
"nofx/backtest"
"nofx/crypto"
"nofx/decision"
"nofx/manager"
"nofx/store"
"nofx/trader"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// Server HTTP API服务器
type Server struct {
router *gin.Engine
traderManager *manager.TraderManager
store *store.Store
cryptoHandler *CryptoHandler
backtestManager *backtest.Manager
httpServer *http.Server
port int
}
// NewServer 创建API服务器
func NewServer(traderManager *manager.TraderManager, st *store.Store, cryptoService *crypto.CryptoService, backtestManager *backtest.Manager, port int) *Server {
// 设置为Release模式减少日志输出
gin.SetMode(gin.ReleaseMode)
router := gin.Default()
// 启用CORS
router.Use(corsMiddleware())
// 创建加密处理器
cryptoHandler := NewCryptoHandler(cryptoService)
s := &Server{
router: router,
traderManager: traderManager,
store: st,
cryptoHandler: cryptoHandler,
backtestManager: backtestManager,
port: port,
}
// 设置路由
s.setupRoutes()
return s
}
// corsMiddleware CORS中间件
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusOK)
return
}
c.Next()
}
}
// setupRoutes 设置路由
func (s *Server) setupRoutes() {
// API路由组
api := s.router.Group("/api")
{
// 健康检查
api.Any("/health", s.handleHealth)
// 管理员登录(管理员模式下使用,公共)
// 系统支持的模型和交易所(无需认证)
api.GET("/supported-models", s.handleGetSupportedModels)
api.GET("/supported-exchanges", s.handleGetSupportedExchanges)
// 系统配置(无需认证,用于前端判断是否管理员模式/注册是否开启)
api.GET("/config", s.handleGetSystemConfig)
// 加密相关接口(无需认证)
api.GET("/crypto/public-key", s.cryptoHandler.HandleGetPublicKey)
api.POST("/crypto/decrypt", s.cryptoHandler.HandleDecryptSensitiveData)
// 系统提示词模板管理(无需认证)
api.GET("/prompt-templates", s.handleGetPromptTemplates)
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)
// 认证相关路由(无需认证)
api.POST("/register", s.handleRegister)
api.POST("/login", s.handleLogin)
api.POST("/verify-otp", s.handleVerifyOTP)
api.POST("/complete-registration", s.handleCompleteRegistration)
// 需要认证的路由
protected := api.Group("/", s.authMiddleware())
{
// 注销(加入黑名单)
protected.POST("/logout", s.handleLogout)
// 服务器IP查询需要认证用于白名单配置
protected.GET("/server-ip", s.handleGetServerIP)
// AI交易员管理
protected.GET("/my-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)
protected.PUT("/traders/:id/prompt", s.handleUpdateTraderPrompt)
protected.POST("/traders/:id/sync-balance", s.handleSyncBalance)
// AI模型配置
protected.GET("/models", s.handleGetModelConfigs)
protected.PUT("/models", s.handleUpdateModelConfigs)
// 交易所配置
protected.GET("/exchanges", s.handleGetExchangeConfigs)
protected.PUT("/exchanges", s.handleUpdateExchangeConfigs)
// 策略管理
protected.GET("/strategies", s.handleGetStrategies)
protected.GET("/strategies/active", s.handleGetActiveStrategy)
protected.GET("/strategies/default-config", s.handleGetDefaultStrategyConfig)
protected.GET("/strategies/templates", s.handleGetPromptTemplates)
protected.POST("/strategies/preview-prompt", s.handlePreviewPrompt)
protected.POST("/strategies/test-run", s.handleStrategyTestRun)
protected.GET("/strategies/:id", s.handleGetStrategy)
protected.POST("/strategies", s.handleCreateStrategy)
protected.PUT("/strategies/:id", s.handleUpdateStrategy)
protected.DELETE("/strategies/:id", s.handleDeleteStrategy)
protected.POST("/strategies/:id/activate", s.handleActivateStrategy)
protected.POST("/strategies/:id/duplicate", s.handleDuplicateStrategy)
// 用户信号源配置
protected.GET("/user/signal-sources", s.handleGetUserSignalSource)
protected.POST("/user/signal-sources", s.handleSaveUserSignalSource)
// 指定trader的数据使用query参数 ?trader_id=xxx
protected.GET("/status", s.handleStatus)
protected.GET("/account", s.handleAccount)
protected.GET("/positions", s.handlePositions)
protected.GET("/decisions", s.handleDecisions)
protected.GET("/decisions/latest", s.handleLatestDecisions)
protected.GET("/statistics", s.handleStatistics)
}
}
}
// handleHealth 健康检查
func (s *Server) handleHealth(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"time": c.Request.Context().Value("time"),
})
}
// handleGetSystemConfig 获取系统配置(客户端需要知道的配置)
func (s *Server) handleGetSystemConfig(c *gin.Context) {
// 获取默认币种
defaultCoinsStr, _ := s.store.SystemConfig().Get("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.store.SystemConfig().Get("btc_eth_leverage")
altcoinLeverageStr, _ := s.store.SystemConfig().Get("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
}
// 获取内测模式配置
betaModeStr, _ := s.store.SystemConfig().Get("beta_mode")
betaMode := betaModeStr == "true"
// 获取注册开关配置(默认开启)
registrationEnabledStr, _ := s.store.SystemConfig().Get("registration_enabled")
registrationEnabled := registrationEnabledStr != "false"
c.JSON(http.StatusOK, gin.H{
"beta_mode": betaMode,
"registration_enabled": registrationEnabled,
"default_coins": defaultCoins,
"btc_eth_leverage": btcEthLeverage,
"altcoin_leverage": altcoinLeverage,
})
}
// handleGetServerIP 获取服务器IP地址用于白名单配置
func (s *Server) handleGetServerIP(c *gin.Context) {
// 尝试通过第三方API获取公网IP
publicIP := getPublicIPFromAPI()
// 如果第三方API失败从网络接口获取第一个公网IP
if publicIP == "" {
publicIP = getPublicIPFromInterface()
}
// 如果还是没有获取到,返回错误
if publicIP == "" {
c.JSON(http.StatusInternalServerError, gin.H{"error": "无法获取公网IP地址"})
return
}
c.JSON(http.StatusOK, gin.H{
"public_ip": publicIP,
"message": "请将此IP地址添加到白名单中",
})
}
// getPublicIPFromAPI 通过第三方API获取公网IP
func getPublicIPFromAPI() string {
// 尝试多个公网IP查询服务
services := []string{
"https://api.ipify.org?format=text",
"https://icanhazip.com",
"https://ifconfig.me",
}
client := &http.Client{
Timeout: 5 * time.Second,
}
for _, service := range services {
resp, err := client.Get(service)
if err != nil {
continue
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
body := make([]byte, 128)
n, err := resp.Body.Read(body)
if err != nil && err.Error() != "EOF" {
continue
}
ip := strings.TrimSpace(string(body[:n]))
// 验证是否为有效的IP地址
if net.ParseIP(ip) != nil {
return ip
}
}
}
return ""
}
// getPublicIPFromInterface 从网络接口获取第一个公网IP
func getPublicIPFromInterface() string {
interfaces, err := net.Interfaces()
if err != nil {
return ""
}
for _, iface := range interfaces {
// 跳过未启用的接口和回环接口
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip == nil || ip.IsLoopback() {
continue
}
// 只考虑IPv4地址
if ip.To4() != nil {
ipStr := ip.String()
// 排除私有IP地址范围
if !isPrivateIP(ip) {
return ipStr
}
}
}
}
return ""
}
// isPrivateIP 判断是否为私有IP地址
func isPrivateIP(ip net.IP) bool {
// 私有IP地址范围
// 10.0.0.0/8
// 172.16.0.0/12
// 192.168.0.0/16
privateRanges := []string{
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
}
for _, cidr := range privateRanges {
_, subnet, _ := net.ParseCIDR(cidr)
if subnet.Contains(ip) {
return true
}
}
return false
}
// getTraderFromQuery 从query参数获取trader
func (s *Server) getTraderFromQuery(c *gin.Context) (*manager.TraderManager, string, error) {
userID := c.GetString("user_id")
traderID := c.Query("trader_id")
// 确保用户的交易员已加载到内存中
err := s.traderManager.LoadUserTradersFromStore(s.store, userID)
if err != nil {
logger.Infof("⚠️ 加载用户 %s 的交易员失败: %v", userID, err)
}
if traderID == "" {
// 如果没有指定trader_id返回该用户的第一个trader
ids := s.traderManager.GetTraderIDs()
if len(ids) == 0 {
return nil, "", fmt.Errorf("没有可用的trader")
}
// 获取用户的交易员列表,优先返回用户自己的交易员
userTraders, err := s.store.Trader().List(userID)
if err == nil && len(userTraders) > 0 {
traderID = userTraders[0].ID
} else {
traderID = ids[0]
}
}
return s.traderManager, traderID, nil
}
// 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"`
StrategyID string `json:"strategy_id"` // 策略ID新版
InitialBalance float64 `json:"initial_balance"`
ScanIntervalMinutes int `json:"scan_interval_minutes"`
IsCrossMargin *bool `json:"is_cross_margin"` // 指针类型nil表示使用默认值true
// 以下字段为向后兼容保留,新版使用策略配置
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"`
SystemPromptTemplate string `json:"system_prompt_template"` // 系统提示词模板名称
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"`
CustomAPIURL string `json:"customApiUrl,omitempty"`
}
// SafeModelConfig 安全的模型配置结构(不包含敏感信息)
type SafeModelConfig struct {
ID string `json:"id"`
Name string `json:"name"`
Provider string `json:"provider"`
Enabled bool `json:"enabled"`
CustomAPIURL string `json:"customApiUrl"` // 自定义API URL通常不敏感
CustomModelName string `json:"customModelName"` // 自定义模型名(不敏感)
}
type ExchangeConfig struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"` // "cex" or "dex"
Enabled bool `json:"enabled"`
APIKey string `json:"apiKey,omitempty"`
SecretKey string `json:"secretKey,omitempty"`
Testnet bool `json:"testnet,omitempty"`
}
// SafeExchangeConfig 安全的交易所配置结构(不包含敏感信息)
type SafeExchangeConfig struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"` // "cex" or "dex"
Enabled bool `json:"enabled"`
Testnet bool `json:"testnet,omitempty"`
HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"` // Hyperliquid钱包地址不敏感
AsterUser string `json:"asterUser"` // Aster用户名不敏感
AsterSigner string `json:"asterSigner"` // Aster签名者不敏感
}
type UpdateModelConfigRequest struct {
Models map[string]struct {
Enabled bool `json:"enabled"`
APIKey string `json:"api_key"`
CustomAPIURL string `json:"custom_api_url"`
CustomModelName string `json:"custom_model_name"`
} `json:"models"`
}
type UpdateExchangeConfigRequest struct {
Exchanges map[string]struct {
Enabled bool `json:"enabled"`
APIKey string `json:"api_key"`
SecretKey string `json:"secret_key"`
Testnet bool `json:"testnet"`
HyperliquidWalletAddr string `json:"hyperliquid_wallet_addr"`
AsterUser string `json:"aster_user"`
AsterSigner string `json:"aster_signer"`
AsterPrivateKey string `json:"aster_private_key"`
LighterWalletAddr string `json:"lighter_wallet_addr"`
LighterPrivateKey string `json:"lighter_private_key"`
} `json:"exchanges"`
}
// handleCreateTrader 创建新的AI交易员
func (s *Server) handleCreateTrader(c *gin.Context) {
userID := c.GetString("user_id")
var req CreateTraderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
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())
// 设置默认值
isCrossMargin := true // 默认为全仓模式
if req.IsCrossMargin != nil {
isCrossMargin = *req.IsCrossMargin
}
// 设置杠杆默认值(从系统配置获取)
btcEthLeverage := 5
altcoinLeverage := 5
if req.BTCETHLeverage > 0 {
btcEthLeverage = req.BTCETHLeverage
} else {
// 从系统配置获取默认值
if btcEthLeverageStr, _ := s.store.SystemConfig().Get("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.store.SystemConfig().Get("altcoin_leverage"); altcoinLeverageStr != "" {
if val, err := strconv.Atoi(altcoinLeverageStr); err == nil && val > 0 {
altcoinLeverage = val
}
}
}
// 设置系统提示词模板默认值
systemPromptTemplate := "default"
if req.SystemPromptTemplate != "" {
systemPromptTemplate = req.SystemPromptTemplate
}
// 设置扫描间隔默认值
scanIntervalMinutes := req.ScanIntervalMinutes
if scanIntervalMinutes < 3 {
scanIntervalMinutes = 3 // 默认3分钟且不允许小于3
}
// ✨ 查询交易所实际余额,覆盖用户输入
actualBalance := req.InitialBalance // 默认使用用户输入
exchanges, err := s.store.Exchange().List(userID)
if err != nil {
logger.Infof("⚠️ 获取交易所配置失败,使用用户输入的初始资金: %v", err)
}
// 查找匹配的交易所配置
var exchangeCfg *store.Exchange
for _, ex := range exchanges {
if ex.ID == req.ExchangeID {
exchangeCfg = ex
break
}
}
if exchangeCfg == nil {
logger.Infof("⚠️ 未找到交易所 %s 的配置,使用用户输入的初始资金", req.ExchangeID)
} else if !exchangeCfg.Enabled {
logger.Infof("⚠️ 交易所 %s 未启用,使用用户输入的初始资金", req.ExchangeID)
} else {
// 根据交易所类型创建临时 trader 查询余额
var tempTrader trader.Trader
var createErr error
switch req.ExchangeID {
case "binance":
tempTrader = trader.NewFuturesTrader(exchangeCfg.APIKey, exchangeCfg.SecretKey, userID)
case "hyperliquid":
tempTrader, createErr = trader.NewHyperliquidTrader(
exchangeCfg.APIKey, // private key
exchangeCfg.HyperliquidWalletAddr,
exchangeCfg.Testnet,
)
case "aster":
tempTrader, createErr = trader.NewAsterTrader(
exchangeCfg.AsterUser,
exchangeCfg.AsterSigner,
exchangeCfg.AsterPrivateKey,
)
case "bybit":
tempTrader = trader.NewBybitTrader(
exchangeCfg.APIKey,
exchangeCfg.SecretKey,
)
default:
logger.Infof("⚠️ 不支持的交易所类型: %s使用用户输入的初始资金", req.ExchangeID)
}
if createErr != nil {
logger.Infof("⚠️ 创建临时 trader 失败,使用用户输入的初始资金: %v", createErr)
} else if tempTrader != nil {
// 查询实际余额
balanceInfo, balanceErr := tempTrader.GetBalance()
if balanceErr != nil {
logger.Infof("⚠️ 查询交易所余额失败,使用用户输入的初始资金: %v", balanceErr)
} else {
// 提取可用余额 - 支持多种字段名格式
if availableBalance, ok := balanceInfo["availableBalance"].(float64); ok && availableBalance > 0 {
// Binance 格式: availableBalance (camelCase)
actualBalance = availableBalance
logger.Infof("✓ 查询到交易所实际余额: %.2f USDT (用户输入: %.2f USDT)", actualBalance, req.InitialBalance)
} else if availableBalance, ok := balanceInfo["available_balance"].(float64); ok && availableBalance > 0 {
// 其他格式: available_balance (snake_case)
actualBalance = availableBalance
logger.Infof("✓ 查询到交易所实际余额: %.2f USDT (用户输入: %.2f USDT)", actualBalance, req.InitialBalance)
} else if totalBalance, ok := balanceInfo["totalWalletBalance"].(float64); ok && totalBalance > 0 {
// Binance 格式: totalWalletBalance (camelCase)
actualBalance = totalBalance
logger.Infof("✓ 查询到交易所总余额: %.2f USDT (用户输入: %.2f USDT)", actualBalance, req.InitialBalance)
} else if totalBalance, ok := balanceInfo["balance"].(float64); ok && totalBalance > 0 {
// 其他格式: balance
actualBalance = totalBalance
logger.Infof("✓ 查询到交易所实际余额: %.2f USDT (用户输入: %.2f USDT)", actualBalance, req.InitialBalance)
} else {
logger.Infof("⚠️ 无法从余额信息中提取可用余额balanceInfo=%v使用用户输入的初始资金", balanceInfo)
}
}
}
}
// 创建交易员配置(数据库实体)
logger.Infof("🔧 DEBUG: 开始创建交易员配置, ID=%s, Name=%s, AIModel=%s, Exchange=%s, StrategyID=%s", traderID, req.Name, req.AIModelID, req.ExchangeID, req.StrategyID)
traderRecord := &store.Trader{
ID: traderID,
UserID: userID,
Name: req.Name,
AIModelID: req.AIModelID,
ExchangeID: req.ExchangeID,
StrategyID: req.StrategyID, // 关联策略ID新版
InitialBalance: actualBalance, // 使用实际查询的余额
BTCETHLeverage: btcEthLeverage,
AltcoinLeverage: altcoinLeverage,
TradingSymbols: req.TradingSymbols,
UseCoinPool: req.UseCoinPool,
UseOITop: req.UseOITop,
CustomPrompt: req.CustomPrompt,
OverrideBasePrompt: req.OverrideBasePrompt,
SystemPromptTemplate: systemPromptTemplate,
IsCrossMargin: isCrossMargin,
ScanIntervalMinutes: scanIntervalMinutes,
IsRunning: false,
}
// 保存到数据库
logger.Infof("🔧 DEBUG: 准备调用 CreateTrader")
err = s.store.Trader().Create(traderRecord)
if err != nil {
logger.Infof("❌ 创建交易员失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("创建交易员失败: %v", err)})
return
}
logger.Infof("🔧 DEBUG: CreateTrader 成功")
// 立即将新交易员加载到TraderManager中
logger.Infof("🔧 DEBUG: 准备调用 LoadUserTraders")
err = s.traderManager.LoadUserTradersFromStore(s.store, userID)
if err != nil {
logger.Infof("⚠️ 加载用户交易员到内存失败: %v", err)
// 这里不返回错误,因为交易员已经成功创建到数据库
}
logger.Infof("🔧 DEBUG: LoadUserTraders 完成")
logger.Infof("✓ 创建交易员成功: %s (模型: %s, 交易所: %s)", req.Name, req.AIModelID, req.ExchangeID)
c.JSON(http.StatusCreated, gin.H{
"trader_id": traderID,
"trader_name": req.Name,
"ai_model": req.AIModelID,
"is_running": false,
})
}
// 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"`
StrategyID string `json:"strategy_id"` // 策略ID新版
InitialBalance float64 `json:"initial_balance"`
ScanIntervalMinutes int `json:"scan_interval_minutes"`
IsCrossMargin *bool `json:"is_cross_margin"`
// 以下字段为向后兼容保留,新版使用策略配置
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"`
SystemPromptTemplate string `json:"system_prompt_template"`
}
// 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.store.Trader().List(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取交易员列表失败"})
return
}
var existingTrader *store.Trader
for _, t := range traders {
if t.ID == traderID {
existingTrader = t
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 // 保持原值
}
// 设置扫描间隔,允许更新
scanIntervalMinutes := req.ScanIntervalMinutes
if scanIntervalMinutes <= 0 {
scanIntervalMinutes = existingTrader.ScanIntervalMinutes // 保持原值
} else if scanIntervalMinutes < 3 {
scanIntervalMinutes = 3
}
// 设置系统提示词模板
systemPromptTemplate := req.SystemPromptTemplate
if systemPromptTemplate == "" {
systemPromptTemplate = existingTrader.SystemPromptTemplate // 保持原值
}
// 处理策略ID如果没有提供保持原值
strategyID := req.StrategyID
if strategyID == "" {
strategyID = existingTrader.StrategyID
}
// 更新交易员配置
traderRecord := &store.Trader{
ID: traderID,
UserID: userID,
Name: req.Name,
AIModelID: req.AIModelID,
ExchangeID: req.ExchangeID,
StrategyID: strategyID, // 关联策略ID
InitialBalance: req.InitialBalance,
BTCETHLeverage: btcEthLeverage,
AltcoinLeverage: altcoinLeverage,
TradingSymbols: req.TradingSymbols,
CustomPrompt: req.CustomPrompt,
OverrideBasePrompt: req.OverrideBasePrompt,
SystemPromptTemplate: systemPromptTemplate,
IsCrossMargin: isCrossMargin,
ScanIntervalMinutes: scanIntervalMinutes,
IsRunning: existingTrader.IsRunning, // 保持原值
}
// 更新数据库
err = s.store.Trader().Update(traderRecord)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新交易员失败: %v", err)})
return
}
// 重新加载交易员到内存
err = s.traderManager.LoadUserTradersFromStore(s.store, userID)
if err != nil {
logger.Infof("⚠️ 重新加载用户交易员到内存失败: %v", err)
}
logger.Infof("✓ 更新交易员成功: %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")
traderID := c.Param("id")
// 从数据库删除
err := s.store.Trader().Delete(userID, traderID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("删除交易员失败: %v", err)})
return
}
// 如果交易员正在运行,先停止它
if trader, err := s.traderManager.GetTrader(traderID); err == nil {
status := trader.GetStatus()
if isRunning, ok := status["is_running"].(bool); ok && isRunning {
trader.Stop()
logger.Infof("⏹ 已停止运行中的交易员: %s", traderID)
}
}
logger.Infof("✓ 交易员已删除: %s", traderID)
c.JSON(http.StatusOK, gin.H{"message": "交易员已删除"})
}
// handleStartTrader 启动交易员
func (s *Server) handleStartTrader(c *gin.Context) {
userID := c.GetString("user_id")
traderID := c.Param("id")
// 校验交易员是否属于当前用户
_, err := s.store.Trader().GetFullConfig(userID, traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在或无访问权限"})
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
return
}
// 检查交易员是否已经在运行
status := trader.GetStatus()
if isRunning, ok := status["is_running"].(bool); ok && isRunning {
c.JSON(http.StatusBadRequest, gin.H{"error": "交易员已在运行中"})
return
}
// 启动交易员
go func() {
logger.Infof("▶️ 启动交易员 %s (%s)", traderID, trader.GetName())
if err := trader.Run(); err != nil {
logger.Infof("❌ 交易员 %s 运行错误: %v", trader.GetName(), err)
}
}()
// 更新数据库中的运行状态
err = s.store.Trader().UpdateStatus(userID, traderID, true)
if err != nil {
logger.Infof("⚠️ 更新交易员状态失败: %v", err)
}
logger.Infof("✓ 交易员 %s 已启动", trader.GetName())
c.JSON(http.StatusOK, gin.H{"message": "交易员已启动"})
}
// handleStopTrader 停止交易员
func (s *Server) handleStopTrader(c *gin.Context) {
userID := c.GetString("user_id")
traderID := c.Param("id")
// 校验交易员是否属于当前用户
_, err := s.store.Trader().GetFullConfig(userID, traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在或无访问权限"})
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
return
}
// 检查交易员是否正在运行
status := trader.GetStatus()
if isRunning, ok := status["is_running"].(bool); ok && !isRunning {
c.JSON(http.StatusBadRequest, gin.H{"error": "交易员已停止"})
return
}
// 停止交易员
trader.Stop()
// 更新数据库中的运行状态
err = s.store.Trader().UpdateStatus(userID, traderID, false)
if err != nil {
logger.Infof("⚠️ 更新交易员状态失败: %v", err)
}
logger.Infof("⏹ 交易员 %s 已停止", trader.GetName())
c.JSON(http.StatusOK, gin.H{"message": "交易员已停止"})
}
// handleUpdateTraderPrompt 更新交易员自定义Prompt
func (s *Server) handleUpdateTraderPrompt(c *gin.Context) {
traderID := c.Param("id")
userID := c.GetString("user_id")
var req struct {
CustomPrompt string `json:"custom_prompt"`
OverrideBasePrompt bool `json:"override_base_prompt"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 更新数据库
err := s.store.Trader().UpdateCustomPrompt(userID, traderID, req.CustomPrompt, req.OverrideBasePrompt)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新自定义prompt失败: %v", err)})
return
}
// 如果trader在内存中更新其custom prompt和override设置
trader, err := s.traderManager.GetTrader(traderID)
if err == nil {
trader.SetCustomPrompt(req.CustomPrompt)
trader.SetOverrideBasePrompt(req.OverrideBasePrompt)
logger.Infof("✓ 已更新交易员 %s 的自定义prompt (覆盖基础=%v)", trader.GetName(), req.OverrideBasePrompt)
}
c.JSON(http.StatusOK, gin.H{"message": "自定义prompt已更新"})
}
// handleSyncBalance 同步交易所余额到initial_balance选项B手动同步 + 选项C智能检测
func (s *Server) handleSyncBalance(c *gin.Context) {
userID := c.GetString("user_id")
traderID := c.Param("id")
logger.Infof("🔄 用户 %s 请求同步交易员 %s 的余额", userID, traderID)
// 从数据库获取交易员配置(包含交易所信息)
fullConfig, err := s.store.Trader().GetFullConfig(userID, traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
return
}
traderConfig := fullConfig.Trader
exchangeCfg := fullConfig.Exchange
if exchangeCfg == nil || !exchangeCfg.Enabled {
c.JSON(http.StatusBadRequest, gin.H{"error": "交易所未配置或未启用"})
return
}
// 创建临时 trader 查询余额
var tempTrader trader.Trader
var createErr error
switch traderConfig.ExchangeID {
case "binance":
tempTrader = trader.NewFuturesTrader(exchangeCfg.APIKey, exchangeCfg.SecretKey, userID)
case "hyperliquid":
tempTrader, createErr = trader.NewHyperliquidTrader(
exchangeCfg.APIKey,
exchangeCfg.HyperliquidWalletAddr,
exchangeCfg.Testnet,
)
case "aster":
tempTrader, createErr = trader.NewAsterTrader(
exchangeCfg.AsterUser,
exchangeCfg.AsterSigner,
exchangeCfg.AsterPrivateKey,
)
case "bybit":
tempTrader = trader.NewBybitTrader(
exchangeCfg.APIKey,
exchangeCfg.SecretKey,
)
default:
c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的交易所类型"})
return
}
if createErr != nil {
logger.Infof("⚠️ 创建临时 trader 失败: %v", createErr)
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("连接交易所失败: %v", createErr)})
return
}
// 查询实际余额
balanceInfo, balanceErr := tempTrader.GetBalance()
if balanceErr != nil {
logger.Infof("⚠️ 查询交易所余额失败: %v", balanceErr)
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("查询余额失败: %v", balanceErr)})
return
}
// 提取可用余额
var actualBalance float64
if availableBalance, ok := balanceInfo["available_balance"].(float64); ok && availableBalance > 0 {
actualBalance = availableBalance
} else if availableBalance, ok := balanceInfo["availableBalance"].(float64); ok && availableBalance > 0 {
actualBalance = availableBalance
} else if totalBalance, ok := balanceInfo["balance"].(float64); ok && totalBalance > 0 {
actualBalance = totalBalance
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "无法获取可用余额"})
return
}
oldBalance := traderConfig.InitialBalance
// ✅ 选项C智能检测余额变化
changePercent := ((actualBalance - oldBalance) / oldBalance) * 100
changeType := "增加"
if changePercent < 0 {
changeType = "减少"
}
logger.Infof("✓ 查询到交易所实际余额: %.2f USDT (当前配置: %.2f USDT, 变化: %.2f%%)",
actualBalance, oldBalance, changePercent)
// 更新数据库中的 initial_balance
err = s.store.Trader().UpdateInitialBalance(userID, traderID, actualBalance)
if err != nil {
logger.Infof("❌ 更新initial_balance失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新余额失败"})
return
}
// 重新加载交易员到内存
err = s.traderManager.LoadUserTradersFromStore(s.store, userID)
if err != nil {
logger.Infof("⚠️ 重新加载用户交易员到内存失败: %v", err)
}
logger.Infof("✅ 已同步余额: %.2f → %.2f USDT (%s %.2f%%)", oldBalance, actualBalance, changeType, changePercent)
c.JSON(http.StatusOK, gin.H{
"message": "余额同步成功",
"old_balance": oldBalance,
"new_balance": actualBalance,
"change_percent": changePercent,
"change_type": changeType,
})
}
// handleGetModelConfigs 获取AI模型配置
func (s *Server) handleGetModelConfigs(c *gin.Context) {
userID := c.GetString("user_id")
logger.Infof("🔍 查询用户 %s 的AI模型配置", userID)
models, err := s.store.AIModel().List(userID)
if err != nil {
logger.Infof("❌ 获取AI模型配置失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("获取AI模型配置失败: %v", err)})
return
}
logger.Infof("✅ 找到 %d 个AI模型配置", len(models))
// 转换为安全的响应结构,移除敏感信息
safeModels := make([]SafeModelConfig, len(models))
for i, model := range models {
safeModels[i] = SafeModelConfig{
ID: model.ID,
Name: model.Name,
Provider: model.Provider,
Enabled: model.Enabled,
CustomAPIURL: model.CustomAPIURL,
CustomModelName: model.CustomModelName,
}
}
c.JSON(http.StatusOK, safeModels)
}
// handleUpdateModelConfigs 更新AI模型配置仅支持加密数据
func (s *Server) handleUpdateModelConfigs(c *gin.Context) {
userID := c.GetString("user_id")
// 读取原始请求体
bodyBytes, err := c.GetRawData()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "读取请求体失败"})
return
}
// 解析加密的 payload
var encryptedPayload crypto.EncryptedPayload
if err := json.Unmarshal(bodyBytes, &encryptedPayload); err != nil {
logger.Infof("❌ 解析加密载荷失败: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "请求格式错误,必须使用加密传输"})
return
}
// 验证是否为加密数据
if encryptedPayload.WrappedKey == "" {
logger.Infof("❌ 检测到非加密请求 (UserID: %s)", userID)
c.JSON(http.StatusBadRequest, gin.H{
"error": "此接口仅支持加密传输,请使用加密客户端",
"code": "ENCRYPTION_REQUIRED",
"message": "Encrypted transmission is required for security reasons",
})
return
}
// 解密数据
decrypted, err := s.cryptoHandler.cryptoService.DecryptSensitiveData(&encryptedPayload)
if err != nil {
logger.Infof("❌ 解密模型配置失败 (UserID: %s): %v", userID, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "解密数据失败"})
return
}
// 解析解密后的数据
var req UpdateModelConfigRequest
if err := json.Unmarshal([]byte(decrypted), &req); err != nil {
logger.Infof("❌ 解析解密数据失败: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "解析解密数据失败"})
return
}
logger.Infof("🔓 已解密模型配置数据 (UserID: %s)", userID)
// 更新每个模型的配置
for modelID, modelData := range req.Models {
err := s.store.AIModel().Update(userID, modelID, modelData.Enabled, modelData.APIKey, modelData.CustomAPIURL, modelData.CustomModelName)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新模型 %s 失败: %v", modelID, err)})
return
}
}
// 重新加载该用户的所有交易员,使新配置立即生效
err = s.traderManager.LoadUserTradersFromStore(s.store, userID)
if err != nil {
logger.Infof("⚠️ 重新加载用户交易员到内存失败: %v", err)
// 这里不返回错误,因为模型配置已经成功更新到数据库
}
logger.Infof("✓ AI模型配置已更新: %+v", req.Models)
c.JSON(http.StatusOK, gin.H{"message": "模型配置已更新"})
}
// handleGetExchangeConfigs 获取交易所配置
func (s *Server) handleGetExchangeConfigs(c *gin.Context) {
userID := c.GetString("user_id")
logger.Infof("🔍 查询用户 %s 的交易所配置", userID)
exchanges, err := s.store.Exchange().List(userID)
if err != nil {
logger.Infof("❌ 获取交易所配置失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("获取交易所配置失败: %v", err)})
return
}
logger.Infof("✅ 找到 %d 个交易所配置", len(exchanges))
// 调试:输出配置详情(脱敏)
for _, ex := range exchanges {
apiKeyMasked := ""
if len(ex.APIKey) > 8 {
apiKeyMasked = ex.APIKey[:8] + "..."
}
secretKeyMasked := ""
if len(ex.SecretKey) > 8 {
secretKeyMasked = ex.SecretKey[:8] + "..."
}
logger.Infof(" └─ 交易所: %s, APIKey: %s, SecretKey: %s", ex.ID, apiKeyMasked, secretKeyMasked)
}
// 打印完整JSON响应用于调试
jsonData, _ := json.Marshal(exchanges)
logger.Infof("📤 完整JSON响应: %s", string(jsonData))
// 转换为安全的响应结构,移除敏感信息
safeExchanges := make([]SafeExchangeConfig, len(exchanges))
for i, exchange := range exchanges {
safeExchanges[i] = SafeExchangeConfig{
ID: exchange.ID,
Name: exchange.Name,
Type: exchange.Type,
Enabled: exchange.Enabled,
Testnet: exchange.Testnet,
HyperliquidWalletAddr: exchange.HyperliquidWalletAddr,
AsterUser: exchange.AsterUser,
AsterSigner: exchange.AsterSigner,
}
}
c.JSON(http.StatusOK, safeExchanges)
}
// handleUpdateExchangeConfigs 更新交易所配置(仅支持加密数据)
func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) {
userID := c.GetString("user_id")
// 读取原始请求体
bodyBytes, err := c.GetRawData()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "读取请求体失败"})
return
}
// 解析加密的 payload
var encryptedPayload crypto.EncryptedPayload
if err := json.Unmarshal(bodyBytes, &encryptedPayload); err != nil {
logger.Infof("❌ 解析加密载荷失败: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "请求格式错误,必须使用加密传输"})
return
}
// 验证是否为加密数据
if encryptedPayload.WrappedKey == "" {
logger.Infof("❌ 检测到非加密请求 (UserID: %s)", userID)
c.JSON(http.StatusBadRequest, gin.H{
"error": "此接口仅支持加密传输,请使用加密客户端",
"code": "ENCRYPTION_REQUIRED",
"message": "Encrypted transmission is required for security reasons",
})
return
}
// 解密数据
decrypted, err := s.cryptoHandler.cryptoService.DecryptSensitiveData(&encryptedPayload)
if err != nil {
logger.Infof("❌ 解密交易所配置失败 (UserID: %s): %v", userID, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "解密数据失败"})
return
}
// 解析解密后的数据
var req UpdateExchangeConfigRequest
if err := json.Unmarshal([]byte(decrypted), &req); err != nil {
logger.Infof("❌ 解析解密数据失败: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "解析解密数据失败"})
return
}
logger.Infof("🔓 已解密交易所配置数据 (UserID: %s)", userID)
// 更新每个交易所的配置
for exchangeID, exchangeData := range req.Exchanges {
err := s.store.Exchange().Update(userID, exchangeID, exchangeData.Enabled, exchangeData.APIKey, exchangeData.SecretKey, exchangeData.Testnet, exchangeData.HyperliquidWalletAddr, exchangeData.AsterUser, exchangeData.AsterSigner, exchangeData.AsterPrivateKey, exchangeData.LighterWalletAddr, exchangeData.LighterPrivateKey)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新交易所 %s 失败: %v", exchangeID, err)})
return
}
}
// 重新加载该用户的所有交易员,使新配置立即生效
err = s.traderManager.LoadUserTradersFromStore(s.store, userID)
if err != nil {
logger.Infof("⚠️ 重新加载用户交易员到内存失败: %v", err)
// 这里不返回错误,因为交易所配置已经成功更新到数据库
}
logger.Infof("✓ 交易所配置已更新: %+v", req.Exchanges)
c.JSON(http.StatusOK, gin.H{"message": "交易所配置已更新"})
}
// handleGetUserSignalSource 获取用户信号源配置
func (s *Server) handleGetUserSignalSource(c *gin.Context) {
userID := c.GetString("user_id")
source, err := s.store.SignalSource().Get(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.store.SignalSource().Create(userID, req.CoinPoolURL, req.OITopURL)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("保存用户信号源配置失败: %v", err)})
return
}
logger.Infof("✓ 用户信号源配置已保存: 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")
traders, err := s.store.Trader().List(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("获取交易员列表失败: %v", err)})
return
}
result := make([]map[string]interface{}, 0, len(traders))
for _, trader := range traders {
// 获取实时运行状态
isRunning := trader.IsRunning
if at, err := s.traderManager.GetTrader(trader.ID); err == nil {
status := at.GetStatus()
if running, ok := status["is_running"].(bool); ok {
isRunning = running
}
}
// 返回完整的 AIModelID如 "admin_deepseek"),不要截断
// 前端需要完整 ID 来验证模型是否存在(与 handleGetTraderConfig 保持一致)
result = append(result, map[string]interface{}{
"trader_id": trader.ID,
"trader_name": trader.Name,
"ai_model": trader.AIModelID, // 使用完整 ID
"exchange_id": trader.ExchangeID,
"is_running": isRunning,
"initial_balance": trader.InitialBalance,
})
}
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
}
fullCfg, err := s.store.Trader().GetFullConfig(userID, traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("获取交易员配置失败: %v", err)})
return
}
traderConfig := fullCfg.Trader
// 获取实时运行状态
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
}
}
// 返回完整的模型ID不做转换保持与前端模型列表一致
aiModelID := traderConfig.AIModelID
result := map[string]interface{}{
"trader_id": traderConfig.ID,
"trader_name": traderConfig.Name,
"ai_model": aiModelID,
"exchange_id": traderConfig.ExchangeID,
"initial_balance": traderConfig.InitialBalance,
"scan_interval_minutes": traderConfig.ScanIntervalMinutes,
"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)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
status := trader.GetStatus()
c.JSON(http.StatusOK, status)
}
// handleAccount 账户信息
func (s *Server) handleAccount(c *gin.Context) {
_, traderID, err := s.getTraderFromQuery(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
logger.Infof("📊 收到账户信息请求 [%s]", trader.GetName())
account, err := trader.GetAccountInfo()
if err != nil {
logger.Infof("❌ 获取账户信息失败 [%s]: %v", trader.GetName(), err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取账户信息失败: %v", err),
})
return
}
logger.Infof("✓ 返回账户信息 [%s]: 净值=%.2f, 可用=%.2f, 盈亏=%.2f (%.2f%%)",
trader.GetName(),
account["total_equity"],
account["available_balance"],
account["total_pnl"],
account["total_pnl_pct"])
c.JSON(http.StatusOK, account)
}
// handlePositions 持仓列表
func (s *Server) handlePositions(c *gin.Context) {
_, traderID, err := s.getTraderFromQuery(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
positions, err := trader.GetPositions()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取持仓列表失败: %v", err),
})
return
}
c.JSON(http.StatusOK, positions)
}
// handleDecisions 决策日志列表
func (s *Server) handleDecisions(c *gin.Context) {
_, traderID, err := s.getTraderFromQuery(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
// 获取所有历史决策记录(无限制)
records, err := trader.GetStore().Decision().GetLatestRecords(trader.GetID(), 10000)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取决策日志失败: %v", err),
})
return
}
c.JSON(http.StatusOK, records)
}
// handleLatestDecisions 最新决策日志最近5条最新的在前
func (s *Server) handleLatestDecisions(c *gin.Context) {
_, traderID, err := s.getTraderFromQuery(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
records, err := trader.GetStore().Decision().GetLatestRecords(trader.GetID(), 5)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取决策日志失败: %v", err),
})
return
}
// 反转数组,让最新的在前面(用于列表显示)
// GetLatestRecords返回的是从旧到新用于图表这里需要从新到旧
for i, j := 0, len(records)-1; i < j; i, j = i+1, j-1 {
records[i], records[j] = records[j], records[i]
}
c.JSON(http.StatusOK, records)
}
// handleStatistics 统计信息
func (s *Server) handleStatistics(c *gin.Context) {
_, traderID, err := s.getTraderFromQuery(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
stats, err := trader.GetStore().Decision().GetStatistics(trader.GetID())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取统计信息失败: %v", err),
})
return
}
c.JSON(http.StatusOK, stats)
}
// handleCompetition 竞赛总览对比所有trader
func (s *Server) handleCompetition(c *gin.Context) {
userID := c.GetString("user_id")
// 确保用户的交易员已加载到内存中
err := s.traderManager.LoadUserTradersFromStore(s.store, userID)
if err != nil {
logger.Infof("⚠️ 加载用户 %s 的交易员失败: %v", userID, err)
}
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)
}
// handleEquityHistory 收益率历史数据
func (s *Server) handleEquityHistory(c *gin.Context) {
_, traderID, err := s.getTraderFromQuery(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
// 获取尽可能多的历史数据(几天的数据)
// 每3分钟一个周期10000条 = 约20天的数据
records, err := trader.GetStore().Decision().GetLatestRecords(trader.GetID(), 10000)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取历史数据失败: %v", err),
})
return
}
// 构建收益率历史数据点
type EquityPoint struct {
Timestamp string `json:"timestamp"`
TotalEquity float64 `json:"total_equity"` // 账户净值wallet + unrealized
AvailableBalance float64 `json:"available_balance"` // 可用余额
TotalPnL float64 `json:"total_pnl"` // 总盈亏(相对初始余额)
TotalPnLPct float64 `json:"total_pnl_pct"` // 总盈亏百分比
PositionCount int `json:"position_count"` // 持仓数量
MarginUsedPct float64 `json:"margin_used_pct"` // 保证金使用率
CycleNumber int `json:"cycle_number"`
}
// 从AutoTrader获取初始余额用于计算盈亏百分比
initialBalance := 0.0
if status := trader.GetStatus(); status != nil {
if ib, ok := status["initial_balance"].(float64); ok && ib > 0 {
initialBalance = ib
}
}
// 如果无法从status获取且有历史记录则从第一条记录获取
if initialBalance == 0 && len(records) > 0 {
// 第一条记录的equity作为初始余额
initialBalance = records[0].AccountState.TotalBalance
}
// 如果还是无法获取,返回错误
if initialBalance == 0 {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "无法获取初始余额",
})
return
}
var history []EquityPoint
for _, record := range records {
// TotalBalance字段实际存储的是TotalEquity
totalEquity := record.AccountState.TotalBalance
// TotalUnrealizedProfit字段实际存储的是TotalPnL相对初始余额
totalPnL := record.AccountState.TotalUnrealizedProfit
// 计算盈亏百分比
totalPnLPct := 0.0
if initialBalance > 0 {
totalPnLPct = (totalPnL / initialBalance) * 100
}
history = append(history, EquityPoint{
Timestamp: record.Timestamp.Format("2006-01-02 15:04:05"),
TotalEquity: totalEquity,
AvailableBalance: record.AccountState.AvailableBalance,
TotalPnL: totalPnL,
TotalPnLPct: totalPnLPct,
PositionCount: record.AccountState.PositionCount,
MarginUsedPct: record.AccountState.MarginUsedPct,
CycleNumber: record.CycleNumber,
})
}
c.JSON(http.StatusOK, history)
}
// authMiddleware JWT认证中间件
func (s *Server) authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "缺少Authorization头"})
c.Abort()
return
}
// 检查Bearer token格式
tokenParts := strings.Split(authHeader, " ")
if len(tokenParts) != 2 || tokenParts[0] != "Bearer" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的Authorization格式"})
c.Abort()
return
}
tokenString := tokenParts[1]
// 黑名单检查
if auth.IsTokenBlacklisted(tokenString) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "token已失效请重新登录"})
c.Abort()
return
}
// 验证JWT token
claims, err := auth.ValidateJWT(tokenString)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的token: " + err.Error()})
c.Abort()
return
}
// 将用户信息存储到上下文中
c.Set("user_id", claims.UserID)
c.Set("email", claims.Email)
c.Next()
}
}
// handleLogout 将当前token加入黑名单
func (s *Server) handleLogout(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "缺少Authorization头"})
return
}
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的Authorization格式"})
return
}
tokenString := parts[1]
claims, err := auth.ValidateJWT(tokenString)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的token"})
return
}
var exp time.Time
if claims.ExpiresAt != nil {
exp = claims.ExpiresAt.Time
} else {
exp = time.Now().Add(24 * time.Hour)
}
auth.BlacklistToken(tokenString, exp)
c.JSON(http.StatusOK, gin.H{"message": "已登出"})
}
// handleRegister 处理用户注册请求
func (s *Server) handleRegister(c *gin.Context) {
var req struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
BetaCode string `json:"beta_code"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 检查是否开启了内测模式
betaModeStr, _ := s.store.SystemConfig().Get("beta_mode")
if betaModeStr == "true" {
// 内测模式下必须提供有效的内测码
if req.BetaCode == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "内测期间,注册需要提供内测码"})
return
}
// 验证内测码
isValid, err := s.store.BetaCode().Validate(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.store.User().GetByEmail(req.Email)
if err == nil {
c.JSON(http.StatusConflict, gin.H{"error": "邮箱已被注册"})
return
}
// 生成密码哈希
passwordHash, err := auth.HashPassword(req.Password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码处理失败"})
return
}
// 生成OTP密钥
otpSecret, err := auth.GenerateOTPSecret()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "OTP密钥生成失败"})
return
}
// 创建用户未验证OTP状态
userID := uuid.New().String()
user := &store.User{
ID: userID,
Email: req.Email,
PasswordHash: passwordHash,
OTPSecret: otpSecret,
OTPVerified: false,
}
err = s.store.User().Create(user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建用户失败: " + err.Error()})
return
}
// 如果是内测模式,标记内测码为已使用
betaModeStr2, _ := s.store.SystemConfig().Get("beta_mode")
if betaModeStr2 == "true" && req.BetaCode != "" {
err := s.store.BetaCode().Use(req.BetaCode, req.Email)
if err != nil {
logger.Infof("⚠️ 标记内测码为已使用失败: %v", err)
// 这里不返回错误,因为用户已经创建成功
} else {
logger.Infof("✓ 内测码 %s 已被用户 %s 使用", req.BetaCode, req.Email)
}
}
// 返回OTP设置信息
qrCodeURL := auth.GetOTPQRCodeURL(otpSecret, req.Email)
c.JSON(http.StatusOK, gin.H{
"user_id": userID,
"email": req.Email,
"otp_secret": otpSecret,
"qr_code_url": qrCodeURL,
"message": "请使用Google Authenticator扫描二维码并验证OTP",
})
}
// handleCompleteRegistration 完成注册验证OTP
func (s *Server) handleCompleteRegistration(c *gin.Context) {
var req struct {
UserID string `json:"user_id" binding:"required"`
OTPCode string `json:"otp_code" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 获取用户信息
user, err := s.store.User().GetByID(req.UserID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
// 验证OTP
if !auth.VerifyOTP(user.OTPSecret, req.OTPCode) {
c.JSON(http.StatusBadRequest, gin.H{"error": "OTP验证码错误"})
return
}
// 更新用户OTP验证状态
err = s.store.User().UpdateOTPVerified(req.UserID, true)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新用户状态失败"})
return
}
// 生成JWT token
token, err := auth.GenerateJWT(user.ID, user.Email)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "生成token失败"})
return
}
// 初始化用户的默认模型和交易所配置
err = s.initUserDefaultConfigs(user.ID)
if err != nil {
logger.Infof("初始化用户默认配置失败: %v", err)
}
c.JSON(http.StatusOK, gin.H{
"token": token,
"user_id": user.ID,
"email": user.Email,
"message": "注册完成",
})
}
// handleLogin 处理用户登录请求
func (s *Server) handleLogin(c *gin.Context) {
var req struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 获取用户信息
user, err := s.store.User().GetByEmail(req.Email)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "邮箱或密码错误"})
return
}
// 验证密码
if !auth.CheckPassword(req.Password, user.PasswordHash) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "邮箱或密码错误"})
return
}
// 检查OTP是否已验证
if !user.OTPVerified {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "账户未完成OTP设置",
"user_id": user.ID,
"requires_otp_setup": true,
})
return
}
// 返回需要OTP验证的状态
c.JSON(http.StatusOK, gin.H{
"user_id": user.ID,
"email": user.Email,
"message": "请输入Google Authenticator验证码",
"requires_otp": true,
})
}
// handleVerifyOTP 验证OTP并完成登录
func (s *Server) handleVerifyOTP(c *gin.Context) {
var req struct {
UserID string `json:"user_id" binding:"required"`
OTPCode string `json:"otp_code" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 获取用户信息
user, err := s.store.User().GetByID(req.UserID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
// 验证OTP
if !auth.VerifyOTP(user.OTPSecret, req.OTPCode) {
c.JSON(http.StatusBadRequest, gin.H{"error": "验证码错误"})
return
}
// 生成JWT token
token, err := auth.GenerateJWT(user.ID, user.Email)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "生成token失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
"user_id": user.ID,
"email": user.Email,
"message": "登录成功",
})
}
// handleResetPassword 重置密码(通过邮箱 + OTP 验证)
func (s *Server) handleResetPassword(c *gin.Context) {
var req struct {
Email string `json:"email" binding:"required,email"`
NewPassword string `json:"new_password" binding:"required,min=6"`
OTPCode string `json:"otp_code" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 查询用户
user, err := s.store.User().GetByEmail(req.Email)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "邮箱不存在"})
return
}
// 验证 OTP
if !auth.VerifyOTP(user.OTPSecret, req.OTPCode) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Google Authenticator 验证码错误"})
return
}
// 生成新密码哈希
newPasswordHash, err := auth.HashPassword(req.NewPassword)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码处理失败"})
return
}
// 更新密码
err = s.store.User().UpdatePassword(user.ID, newPasswordHash)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码更新失败"})
return
}
logger.Infof("✓ 用户 %s 密码已重置", user.Email)
c.JSON(http.StatusOK, gin.H{"message": "密码重置成功,请使用新密码登录"})
}
// initUserDefaultConfigs 为新用户初始化默认的模型和交易所配置
func (s *Server) initUserDefaultConfigs(userID string) error {
// 注释掉自动创建默认配置,让用户手动添加
// 这样新用户注册后不会自动有配置项
logger.Infof("用户 %s 注册完成等待手动配置AI模型和交易所", userID)
return nil
}
// handleGetSupportedModels 获取系统支持的AI模型列表
func (s *Server) handleGetSupportedModels(c *gin.Context) {
// 返回系统支持的AI模型从default用户获取
models, err := s.store.AIModel().List("default")
if err != nil {
logger.Infof("❌ 获取支持的AI模型失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取支持的AI模型失败"})
return
}
c.JSON(http.StatusOK, models)
}
// handleGetSupportedExchanges 获取系统支持的交易所列表
func (s *Server) handleGetSupportedExchanges(c *gin.Context) {
// 返回系统支持的交易所从default用户获取
exchanges, err := s.store.Exchange().List("default")
if err != nil {
logger.Infof("❌ 获取支持的交易所失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取支持的交易所失败"})
return
}
// 转换为安全的响应结构,移除敏感信息
safeExchanges := make([]SafeExchangeConfig, len(exchanges))
for i, exchange := range exchanges {
safeExchanges[i] = SafeExchangeConfig{
ID: exchange.ID,
Name: exchange.Name,
Type: exchange.Type,
Enabled: exchange.Enabled,
Testnet: exchange.Testnet,
HyperliquidWalletAddr: "", // 默认配置不包含钱包地址
AsterUser: "", // 默认配置不包含用户信息
AsterSigner: "",
}
}
c.JSON(http.StatusOK, safeExchanges)
}
// Start 启动服务器
func (s *Server) Start() error {
addr := fmt.Sprintf(":%d", s.port)
logger.Infof("🌐 API服务器启动在 http://localhost%s", addr)
logger.Infof("📊 API文档:")
logger.Infof(" • GET /api/health - 健康检查")
logger.Infof(" • GET /api/traders - 公开的AI交易员排行榜前50名无需认证")
logger.Infof(" • GET /api/competition - 公开的竞赛数据(无需认证)")
logger.Infof(" • GET /api/top-traders - 前5名交易员数据无需认证表现对比用")
logger.Infof(" • GET /api/equity-history?trader_id=xxx - 公开的收益率历史数据(无需认证,竞赛用)")
logger.Infof(" • GET /api/equity-history-batch?trader_ids=a,b,c - 批量获取历史数据(无需认证,表现对比优化)")
logger.Infof(" • GET /api/traders/:id/public-config - 公开的交易员配置(无需认证,不含敏感信息)")
logger.Infof(" • POST /api/traders - 创建新的AI交易员")
logger.Infof(" • DELETE /api/traders/:id - 删除AI交易员")
logger.Infof(" • POST /api/traders/:id/start - 启动AI交易员")
logger.Infof(" • POST /api/traders/:id/stop - 停止AI交易员")
logger.Infof(" • GET /api/models - 获取AI模型配置")
logger.Infof(" • PUT /api/models - 更新AI模型配置")
logger.Infof(" • GET /api/exchanges - 获取交易所配置")
logger.Infof(" • PUT /api/exchanges - 更新交易所配置")
logger.Infof(" • GET /api/status?trader_id=xxx - 指定trader的系统状态")
logger.Infof(" • GET /api/account?trader_id=xxx - 指定trader的账户信息")
logger.Infof(" • GET /api/positions?trader_id=xxx - 指定trader的持仓列表")
logger.Infof(" • GET /api/decisions?trader_id=xxx - 指定trader的决策日志")
logger.Infof(" • GET /api/decisions/latest?trader_id=xxx - 指定trader的最新决策")
logger.Infof(" • GET /api/statistics?trader_id=xxx - 指定trader的统计信息")
logger.Infof(" • GET /api/performance?trader_id=xxx - 指定trader的AI学习表现分析")
logger.Info()
s.httpServer = &http.Server{
Addr: addr,
Handler: s.router,
}
return s.httpServer.ListenAndServe()
}
// Shutdown 优雅关闭服务器
func (s *Server) Shutdown() error {
if s.httpServer == nil {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return s.httpServer.Shutdown(ctx)
}
// handleGetPromptTemplates 获取所有系统提示词模板列表
func (s *Server) handleGetPromptTemplates(c *gin.Context) {
// 导入 decision 包
templates := decision.GetAllPromptTemplates()
// 转换为响应格式
response := make([]map[string]interface{}, 0, len(templates))
for _, tmpl := range templates {
response = append(response, map[string]interface{}{
"name": tmpl.Name,
})
}
c.JSON(http.StatusOK, gin.H{
"templates": response,
})
}
// handleGetPromptTemplate 获取指定名称的提示词模板内容
func (s *Server) handleGetPromptTemplate(c *gin.Context) {
templateName := c.Param("name")
template, err := decision.GetPromptTemplate(templateName)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("模板不存在: %s", templateName)})
return
}
c.JSON(http.StatusOK, gin.H{
"name": template.Name,
"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.GetStore().Decision().GetLatestRecords(trader.GetID(), 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)
}