Skip to content

WebSocket K-line monitor follow-up improvements #260

@xqliu

Description

@xqliu

背景 / Background

PR #176 已经成功合并,实现了 WebSocket K线数据缓存,性能提升显著:

  • ⚡ 延迟从 2-5分钟降至 <100ms
  • 💰 API 消耗接近 0%
  • 📊 实时数据推送

但在 code review 中发现了一些需要后续优化的问题。


需要修复的问题 / Issues to Fix

P0 - Critical (必须修复)

1. ❌ market.Get() 缺少 nil 检查

位置: market/data.go:21

问题:

func Get(symbol string) (*Data, error) {
    klines3m, err = WSMonitorCli.GetCurrentKlines(symbol, "3m")  // ← 如果 WSMonitorCli 为 nil 会 panic

影响: 如果 WebSocket 监控器未初始化就调用 market.Get(),程序会 panic

修复:

func Get(symbol string) (*Data, error) {
    if WSMonitorCli == nil {
        return nil, fmt.Errorf("WebSocket监控器未初始化")
    }
    
    klines3m, err = WSMonitorCli.GetCurrentKlines(symbol, "3m")
    // ...
}

P1 - High Priority (强烈建议)

2. 🔄 缺少动态币种订阅机制

问题:

  • 当前只在启动时订阅 database.GetCustomCoins()
  • 如果运行时通过 Web UI 添加新交易员或修改币种配置
  • 新币种不会被自动订阅

影响:

  • 新添加的交易员无法获取 K线数据
  • 用户体验差

建议实现:

// market/monitor.go
func (m *WSMonitor) AddSymbol(symbol string) error {
    // 检查是否已订阅
    if _, exists := m.klineDataMap3m.Load(symbol); exists {
        return nil // 已存在
    }
    
    // 获取历史数据
    apiClient := NewAPIClient()
    klines3m, err := apiClient.GetKlines(symbol, "3m", 100)
    if err != nil {
        return fmt.Errorf("获取历史数据失败: %v", err)
    }
    
    klines4h, err := apiClient.GetKlines(symbol, "4h", 100)
    if err != nil {
        return fmt.Errorf("获取历史数据失败: %v", err)
    }
    
    // 存储历史数据
    m.klineDataMap3m.Store(symbol, klines3m)
    m.klineDataMap4h.Store(symbol, klines4h)
    
    // 订阅新币种
    m.subscribeSymbol(symbol, "3m")
    m.subscribeSymbol(symbol, "4h")
    
    return nil
}

// manager/trader_manager.go
func (tm *TraderManager) StartTrader(traderID string) error {
    // ... 启动交易员逻辑
    
    // 动态添加币种订阅
    for _, symbol := range trader.GetTradingSymbols() {
        if err := market.WSMonitorCli.AddSymbol(symbol); err != nil {
            log.Printf("⚠️  添加币种订阅失败 %s: %v", symbol, err)
        }
    }
    
    return nil
}

3. 🧹 缺少优雅关闭机制

位置: market/monitor.go

问题: 没有 Stop() 方法来清理 WebSocket 连接和 goroutines

影响:

  • 程序退出时 WebSocket 连接未关闭
  • goroutines 泄漏

建议实现:

// market/monitor.go
func (m *WSMonitor) Stop() error {
    log.Println("📴 正在关闭 WebSocket 监控器...")
    
    // 取消所有订阅
    if m.combinedClient != nil {
        m.combinedClient.Close()
    }
    
    // 关闭 WebSocket 连接
    if m.wsClient != nil && m.wsClient.conn != nil {
        m.wsClient.conn.Close()
    }
    
    // 关闭 channels
    close(m.alertsChan)
    
    log.Println("✅ WebSocket 监控器已关闭")
    return nil
}
// main.go
func main() {
    // ... 启动逻辑
    
    // 启动 WebSocket 监控器
    go market.NewWSMonitor(150).Start(database.GetCustomCoins())
    
    // 设置优雅退出
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
    
    <-sigChan
    log.Println("🛑 收到退出信号...")
    
    // 优雅关闭
    if market.WSMonitorCli != nil {
        market.WSMonitorCli.Stop()
    }
    
    os.Exit(0)
}

4. 🐌 币种去重算法低效

位置: config/database.go:950

问题:

for _, s := range strings.Split(symbol, ",") {
    if s == "" {
        continue
    }
    coin := market.Normalize(s)
    if !slices.Contains(symbols, coin) {  // ← O(n²) 复杂度
        symbols = append(symbols, coin)
    }
}

影响: 当币种数量较多时性能差

建议优化:

func (d *Database) GetCustomCoins() []string {
    var symbol string
    var symbols []string
    _ = d.db.QueryRow(`
        SELECT GROUP_CONCAT(custom_coins , ',') as symbol
        FROM main.traders where custom_coins != ''
    `).Scan(&symbol)
    
    // 检测用户是否未配置币种
    if symbol == "" {
        symbolJSON, _ := d.GetSystemConfig("default_coins")
        if err := json.Unmarshal([]byte(symbolJSON), &symbols); err != nil {
            log.Printf("⚠️  解析default_coins配置失败: %v,使用硬编码默认值", err)
            symbols = []string{"BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT"}
        }
        return symbols
    }
    
    // 使用 map 去重 - O(n) 复杂度
    symbolMap := make(map[string]bool)
    
    // 添加已有的默认币种
    for _, s := range symbols {
        symbolMap[s] = true
    }
    
    // 添加用户自定义币种
    for _, s := range strings.Split(symbol, ",") {
        if s == "" {
            continue
        }
        coin := market.Normalize(s)
        symbolMap[coin] = true
    }
    
    // 转换为切片
    result := make([]string, 0, len(symbolMap))
    for coin := range symbolMap {
        result = append(result, coin)
    }
    
    return result
}

P2 - Medium Priority (优化建议)

5. ⏱️ 缺少数据新鲜度检查

问题: 缓存的 K线数据没有时间戳验证

影响: 如果 WebSocket 断开较长时间,缓存数据会过时但仍被使用

建议: 添加数据新鲜度检查

func (m *WSMonitor) GetCurrentKlines(symbol, interval string) ([]Kline, error) {
    // 获取缓存
    var klines []Kline
    if interval == "3m" {
        data, ok := m.klineDataMap3m.Load(symbol)
        if !ok {
            return nil, fmt.Errorf("未找到币种: %s", symbol)
        }
        klines = data.([]Kline)
    }
    
    // 检查数据新鲜度
    if len(klines) > 0 {
        lastKline := klines[len(klines)-1]
        age := time.Since(time.Unix(lastKline.CloseTime/1000, 0))
        
        maxAge := 5 * time.Minute  // 3m K线最多5分钟过期
        if interval == "4h" {
            maxAge = 10 * time.Minute
        }
        
        if age > maxAge {
            log.Printf("⚠️  %s K线数据过期 (%.1f分钟前), 需要刷新", symbol, age.Minutes())
            // 可以选择回退到 REST API 或返回错误
        }
    }
    
    return klines, nil
}

6. 🚀 初始化并发数可优化

位置: market/monitor.go:86

当前:

semaphore := make(chan struct{}, 5) // 并发数仅5

建议: 提高到 10-20,加快初始化速度


实施建议 / Implementation Plan

  1. 阶段1: 修复 P0 问题(nil 检查)- 立即
  2. 阶段2: 实现 P1 功能(动态订阅、优雅关闭)- 本周
  3. 阶段3: 优化 P2 项目(数据新鲜度、性能)- 下周

参考 / References


优先级: P0 + P1 建议在下一个版本修复

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions