Files
shinchan-zhai 5d6ec35bb4 fix(agent): address critical issues from PR #1485 review
1. Prevent double-close panic on Stop() by using sync.Once in Scheduler,
   Brain, and Sentinel; remove duplicate Stop() call in main.go
2. Add trade quantity (100k) and leverage (125x) sanity caps to prevent
   LLM hallucinations or input errors from reaching the exchange
3. Mask secrets in onboarding setup state — only store "****" markers in
   SystemConfig instead of plaintext API keys/secrets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-22 00:01:46 +08:00

202 lines
6.1 KiB
Go

package main
import (
"log/slog"
"nofx/api"
nofxiagent "nofx/agent"
"nofx/auth"
"nofx/config"
"nofx/crypto"
"nofx/logger"
"nofx/manager"
"nofx/telemetry"
_ "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("║ 🚀 NOFX - AI-Powered Trading System ║")
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"
}
idShort := t.ID
if len(idShort) > 8 {
idShort = idShort[:8]
}
logger.Infof(" • %s [%s] %s - AI Model: %s, Exchange: %s",
t.Name, idShort, 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)
go func() {
if err := server.Start(); err != nil {
logger.Fatalf("❌ Failed to start API server: %v", err)
}
}()
// Start the NOFXi web agent on top of the current dev branch services.
nofxiAgent := nofxiagent.New(traderManager, st, nil, slog.Default())
nofxiAgent.Start()
defer nofxiAgent.Stop()
agentWeb := nofxiagent.NewWebHandler(nofxiAgent, slog.Default())
server.RegisterAgentHandler(agentWeb)
// Start Telegram bot (if TELEGRAM_BOT_TOKEN is configured)
go telegram.Start(cfg, st, telegramReloadCh)
// 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...")
if err := server.Shutdown(); err != nil {
logger.Warnf("⚠️ HTTP server shutdown error: %v", err)
}
logger.Info("✅ HTTP server stopped")
// nofxiAgent.Stop() is handled by defer above
// 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)
}