refactor: simplify config and remove unused database tables

- Remove system_config, beta_codes, signal_source tables and related code
- Simplify config.go to only read from .env (APIServerPort, JWTSecret, RegistrationEnabled)
- Remove GetCustomCoins, use all USDT perpetual contracts for WSMonitor
- Add trader_equity_snapshots table for equity tracking
- Remove signal source modal from frontend AITradersPage
- Fix WSMonitor nil panic by restoring initialization in main.go
This commit is contained in:
tinkle-community
2025-12-07 20:17:03 +08:00
parent 07ac8e4ecd
commit 2334d78e4a
15 changed files with 490 additions and 1493 deletions

View File

@@ -4,17 +4,17 @@ import (
"context"
"encoding/json"
"fmt"
"nofx/logger"
"net"
"net/http"
"nofx/auth"
"nofx/backtest"
"nofx/config"
"nofx/crypto"
"nofx/decision"
"nofx/logger"
"nofx/manager"
"nofx/store"
"nofx/trader"
"strconv"
"strings"
"time"
@@ -159,10 +159,6 @@ func (s *Server) setupRoutes() {
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)
@@ -184,45 +180,12 @@ func (s *Server) handleHealth(c *gin.Context) {
// 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"
cfg := config.Get()
c.JSON(http.StatusOK, gin.H{
"beta_mode": betaMode,
"registration_enabled": registrationEnabled,
"default_coins": defaultCoins,
"btc_eth_leverage": btcEthLeverage,
"altcoin_leverage": altcoinLeverage,
"registration_enabled": cfg.RegistrationEnabled,
"btc_eth_leverage": 10, // 默认值
"altcoin_leverage": 5, // 默认值
})
}
@@ -510,28 +473,14 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
isCrossMargin = *req.IsCrossMargin
}
// 设置杠杆默认值(从系统配置获取)
btcEthLeverage := 5
altcoinLeverage := 5
// 设置杠杆默认值
btcEthLeverage := 10 // 默认值
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
}
}
}
// 设置系统提示词模板默认值
@@ -1424,48 +1373,6 @@ 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.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")
@@ -1731,6 +1638,7 @@ func (s *Server) handleCompetition(c *gin.Context) {
}
// handleEquityHistory 收益率历史数据
// 直接从数据库查询,不依赖内存中的 trader这样重启后也能获取历史数据
func (s *Server) handleEquityHistory(c *gin.Context) {
_, traderID, err := s.getTraderFromQuery(c)
if err != nil {
@@ -1738,15 +1646,9 @@ func (s *Server) handleEquityHistory(c *gin.Context) {
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
// 获取尽可能多的历史数据(几天的数据)
// 从新的 equity 表获取净值历史数据
// 每3分钟一个周期10000条 = 约20天的数据
records, err := trader.GetStore().Decision().GetLatestRecords(trader.GetID(), 10000)
snapshots, err := s.store.Equity().GetLatest(traderID, 10000)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取历史数据失败: %v", err),
@@ -1754,62 +1656,44 @@ func (s *Server) handleEquityHistory(c *gin.Context) {
return
}
if len(snapshots) == 0 {
c.JSON(http.StatusOK, []interface{}{})
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"` // 总盈亏(相对初始余额
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
}
// 如果还是无法获取,返回错误
// 使用第一条记录的余额作为初始余额来计算收益率
initialBalance := snapshots[0].Balance
if initialBalance == 0 {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "无法获取初始余额",
})
return
initialBalance = 1 // 避免除零
}
var history []EquityPoint
for _, record := range records {
// TotalBalance字段实际存储的是TotalEquity
totalEquity := record.AccountState.TotalBalance
// TotalUnrealizedProfit字段实际存储的是TotalPnL相对初始余额
totalPnL := record.AccountState.TotalUnrealizedProfit
for _, snap := range snapshots {
// 计算盈亏百分比
totalPnLPct := 0.0
if initialBalance > 0 {
totalPnLPct = (totalPnL / initialBalance) * 100
totalPnLPct = (snap.UnrealizedPnL / initialBalance) * 100
}
history = append(history, EquityPoint{
Timestamp: record.Timestamp.Format("2006-01-02 15:04:05"),
TotalEquity: totalEquity,
AvailableBalance: record.AccountState.AvailableBalance,
TotalPnL: totalPnL,
Timestamp: snap.Timestamp.Format("2006-01-02 15:04:05"),
TotalEquity: snap.TotalEquity,
AvailableBalance: snap.Balance,
TotalPnL: snap.UnrealizedPnL,
TotalPnLPct: totalPnLPct,
PositionCount: record.AccountState.PositionCount,
MarginUsedPct: record.AccountState.MarginUsedPct,
CycleNumber: record.CycleNumber,
PositionCount: snap.PositionCount,
MarginUsedPct: snap.MarginUsedPct,
})
}
@@ -1889,11 +1773,15 @@ func (s *Server) handleLogout(c *gin.Context) {
// handleRegister 处理用户注册请求
func (s *Server) handleRegister(c *gin.Context) {
// 检查是否允许注册
if !config.Get().RegistrationEnabled {
c.JSON(http.StatusForbidden, gin.H{"error": "注册功能已关闭"})
return
}
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 {
@@ -1901,27 +1789,6 @@ func (s *Server) handleRegister(c *gin.Context) {
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 {
@@ -1959,18 +1826,6 @@ func (s *Server) handleRegister(c *gin.Context) {
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{
@@ -2420,6 +2275,7 @@ func (s *Server) handleEquityHistoryBatch(c *gin.Context) {
}
// getEquityHistoryForTraders 获取多个交易员的历史数据
// 直接从数据库查询,不依赖内存中的 trader这样重启后也能获取历史数据
func (s *Server) getEquityHistoryForTraders(traderIDs []string) map[string]interface{} {
result := make(map[string]interface{})
histories := make(map[string]interface{})
@@ -2430,30 +2286,27 @@ func (s *Server) getEquityHistoryForTraders(traderIDs []string) map[string]inter
continue
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
errors[traderID] = "交易员不存在"
continue
}
// 获取历史数据(用于对比展示,限制数据量)
records, err := trader.GetStore().Decision().GetLatestRecords(trader.GetID(), 500)
// 从新的 equity 表获取净值历史数据
snapshots, err := s.store.Equity().GetLatest(traderID, 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
if len(snapshots) == 0 {
// 没有历史记录,返回空数组
histories[traderID] = []map[string]interface{}{}
continue
}
// 构建收益率历史数据
history := make([]map[string]interface{}, 0, len(snapshots))
for _, snap := range snapshots {
history = append(history, map[string]interface{}{
"timestamp": record.Timestamp,
"total_equity": totalEquity,
"total_pnl": record.AccountState.TotalUnrealizedProfit,
"balance": record.AccountState.TotalBalance,
"timestamp": snap.Timestamp,
"total_equity": snap.TotalEquity,
"total_pnl": snap.UnrealizedPnL,
"balance": snap.Balance,
})
}