mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-07 04:50:57 +08:00
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.
212 lines
6.6 KiB
Go
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)
|
|
}
|