Files
nofx/main.go
shinchan-zhai fd28aa7fc6 reliability: wrap all 27 bare goroutines with safe.Go/GoNamed panic recovery
Applied safe.GoNamed to:
- 9 exchange order_sync goroutines (OKX, Hyperliquid, Aster, Bybit, KuCoin, Gate, Bitget, Lighter, Binance×2)
- Drawdown monitor (auto_trader_risk.go)
- Brain news scanner + market briefs (agent/brain.go)
- Sentinel scanner (agent/sentinel.go)
- Agent scheduler (agent/scheduler.go)
- x402 idle watchdog (mcp/payment/x402.go)
- MCP stream idle watchdog (mcp/client.go)
- Rate limiter cleanup (api/rate_limiter.go)
- 3 telemetry fire-and-forget sends (telemetry/experience.go)
- CoinAnk WS handler (provider/coinank/coinank_api/kline_ws.go)
- API server goroutine (main.go)

Added manual defer/recover with error reporting to:
- Telegram AI agent handler (sends error msg to user on panic)
- Trader data fetch (returns error result on panic to prevent deadlock)

Before: a panic in ANY of these 27 goroutines would crash the entire
trading process with zero diagnostics. Now all panics are caught, logged
with stack traces, and the process continues running.
2026-03-25 01:05:54 +08:00

212 lines
6.6 KiB
Go

package main
import (
"log/slog"
"nofx/api"
nofxiagent "nofx/agent"
"nofx/auth"
"nofx/config"
"nofx/crypto"
"nofx/telemetry"
"nofx/logger"
"nofx/manager"
"nofx/safe"
_ "nofx/mcp/payment"
_ "nofx/mcp/provider"
"nofx/store"
"nofx/telegram"
"os"
"os/signal"
"path/filepath"
"syscall"
"github.com/google/uuid"
"github.com/joho/godotenv"
)
func main() {
// Load .env environment variables
_ = godotenv.Load()
// Initialize logger
logger.Init(nil)
logger.Info("╔══════════════════════════════════════════════════╗")
logger.Info("║ 🚀 NOFXi - AI Trading Agent ║")
logger.Info("║ Powered by NOFX Engine ║")
logger.Info("╚══════════════════════════════════════════════════╝")
// Initialize global configuration (loaded from .env)
config.Init()
cfg := config.Get()
logger.Info("✅ Configuration loaded")
// Initialize encryption service BEFORE database (so EncryptedString can decrypt on read)
logger.Info("🔐 Initializing encryption service...")
cryptoService, err := crypto.NewCryptoService()
if err != nil {
logger.Fatalf("❌ Failed to initialize encryption service: %v", err)
}
crypto.SetGlobalCryptoService(cryptoService)
logger.Info("✅ Encryption service initialized successfully")
// Initialize database from configuration
// For backward compatibility: command line arg overrides config (SQLite only)
if len(os.Args) > 1 {
cfg.DBPath = os.Args[1]
}
// Ensure data directory exists (for SQLite)
if cfg.DBType == "sqlite" {
if dir := filepath.Dir(cfg.DBPath); dir != "." {
if err := os.MkdirAll(dir, 0755); err != nil {
logger.Errorf("Failed to create data directory: %v", err)
}
}
}
logger.Infof("📋 Initializing database (%s)...", cfg.DBType)
dbType := store.DBTypeSQLite
if cfg.DBType == "postgres" {
dbType = store.DBTypePostgres
}
st, err := store.NewWithConfig(store.DBConfig{
Type: dbType,
Path: cfg.DBPath,
Host: cfg.DBHost,
Port: cfg.DBPort,
User: cfg.DBUser,
Password: cfg.DBPassword,
DBName: cfg.DBName,
SSLMode: cfg.DBSSLMode,
})
if err != nil {
logger.Fatalf("❌ Failed to initialize database: %v", err)
}
defer st.Close()
// Initialize installation ID for experience improvement (anonymous statistics)
initInstallationID(st)
// Set JWT secret
auth.SetJWTSecret(cfg.JWTSecret)
logger.Info("🔑 JWT secret configured")
// WebSocket market monitor is NO LONGER USED
// All K-line data now comes from CoinAnk API instead of Binance WebSocket cache
// Commented out to reduce unnecessary connections:
// go market.NewWSMonitor(150).Start(nil)
// logger.Info("📊 WebSocket market monitor started")
// time.Sleep(500 * time.Millisecond)
logger.Info("📊 Using CoinAnk API for all market data (WebSocket cache disabled)")
// Create TraderManager
traderManager := manager.NewTraderManager()
// Load all traders from database to memory (may auto-start traders with IsRunning=true)
if err := traderManager.LoadTradersFromStore(st); err != nil {
logger.Fatalf("❌ Failed to load traders: %v", err)
}
// Display loaded trader information
traders, err := st.Trader().List("default")
if err != nil {
logger.Fatalf("❌ Failed to get trader list: %v", err)
}
logger.Info("🤖 AI Trader Configurations in Database:")
if len(traders) == 0 {
logger.Info(" (No trader configurations, please create via Web interface)")
} else {
for _, t := range traders {
status := "❌ Stopped"
if t.IsRunning {
status = "✅ Running"
}
tid := t.ID
if len(tid) > 8 { tid = tid[:8] }
logger.Infof(" • %s [%s] %s - AI Model: %s, Exchange: %s",
t.Name, tid, status, t.AIModelID, t.ExchangeID)
}
}
// Start API server
server := api.NewServer(traderManager, st, cryptoService, cfg.APIServerPort)
// Create hot-reload channel for Telegram bot; wire it to the API server
// so that POST /api/telegram can trigger a bot restart when the token changes.
telegramReloadCh := make(chan struct{}, 1)
server.SetTelegramReloadCh(telegramReloadCh)
safe.GoNamed("api-server", func() {
if err := server.Start(); err != nil {
logger.Fatalf("❌ Failed to start API server: %v", err)
}
})
// Start NOFXi Agent (proactive intelligence layer) — must be created BEFORE Telegram bot
nofxiAgent := nofxiagent.New(traderManager, st, nil, slog.Default())
// Try to get an AI client from existing trader configs
for _, t := range traderManager.GetAllTraders() {
if mcpClient := t.GetMCPClient(); mcpClient != nil {
nofxiAgent.SetAIClient(mcpClient)
logger.Info("🧠 NOFXi Agent using AI model from trader", "trader", t.GetName())
break
}
}
nofxiAgent.Start()
defer nofxiAgent.Stop()
// Register NOFXi Agent API on the main server's router
agentWeb := nofxiagent.NewWebHandler(nofxiAgent, slog.Default())
server.RegisterAgentHandler(agentWeb)
// Wire NOFXi Agent into Telegram bot — messages route through the unified agent
telegram.SetNOFXiAgent(nofxiAgent)
logger.Info("🔗 NOFXi Agent connected to Telegram bot")
// Start Telegram bot (if token is configured)
go telegram.Start(cfg, st, telegramReloadCh)
logger.Info("🧠 NOFXi Agent started (sentinel + brain + scheduler + web:8900)")
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
logger.Info("✅ System started successfully, waiting for trading commands...")
logger.Info("📌 Tip: Use Ctrl+C to stop the system")
<-quit
logger.Info("📴 Shutdown signal received, closing system...")
// Stop all traders
traderManager.StopAll()
logger.Info("✅ System shut down safely")
}
// initInstallationID initializes the anonymous installation ID for experience improvement
// This ID is persisted in database and used for anonymous usage statistics
func initInstallationID(st *store.Store) {
const key = "installation_id"
// Try to load from database
installationID, err := st.GetSystemConfig(key)
if err != nil {
logger.Warnf("⚠️ Failed to load installation ID: %v", err)
}
// Generate new ID if not exists
if installationID == "" {
installationID = uuid.New().String()
if err := st.SetSystemConfig(key, installationID); err != nil {
logger.Warnf("⚠️ Failed to save installation ID: %v", err)
}
logger.Infof("📊 Generated new installation ID: %s", installationID[:8]+"...")
}
// Set installation ID in experience module
telemetry.SetInstallationID(installationID)
}