mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-10 14:27:00 +08:00
- Add assistant package with AI Agent runtime - agent.go: Core agent loop with tool calling - session.go: Conversation memory management - tool.go: Tool interface and base implementation - trading_tools.go: Trading-specific tools (13 tools) - prompts.go: Trading expert system prompts (EN/ZH) - Add telegram package for Telegram bot integration - bot.go: Telegram bot with rate limiting & access control - config.go: Environment-based configuration - Update main.go to initialize Telegram bot on startup - Update .env.example with new configuration options - Add gopkg.in/telebot.v3 dependency Trading tools available: - Query: get_balance, get_positions, list_traders, get_trader_status - Control: start_trader, stop_trader - Trading: get_market_price, open_long, open_short, close_position - Config: list_strategies, list_exchanges, list_ai_models
278 lines
8.5 KiB
Go
278 lines
8.5 KiB
Go
package main
|
||
|
||
import (
|
||
"nofx/api"
|
||
"nofx/assistant"
|
||
"nofx/auth"
|
||
"nofx/backtest"
|
||
"nofx/config"
|
||
"nofx/crypto"
|
||
"nofx/experience"
|
||
"nofx/logger"
|
||
"nofx/manager"
|
||
"nofx/mcp"
|
||
"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()
|
||
backtest.UseDatabaseWithType(st.DB(), st.DBType() == store.DBTypePostgres)
|
||
|
||
// 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 and BacktestManager
|
||
traderManager := manager.NewTraderManager()
|
||
mcpClient := newSharedMCPClient()
|
||
backtestManager := backtest.NewManager(mcpClient)
|
||
if err := backtestManager.RestoreRuns(); err != nil {
|
||
logger.Warnf("⚠️ Failed to restore backtest history: %v", err)
|
||
}
|
||
|
||
// 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"
|
||
}
|
||
logger.Infof(" • %s [%s] %s - AI Model: %s, Exchange: %s",
|
||
t.Name, t.ID[:8], status, t.AIModelID, t.ExchangeID)
|
||
}
|
||
}
|
||
|
||
// Start API server
|
||
server := api.NewServer(traderManager, st, cryptoService, backtestManager, cfg.APIServerPort)
|
||
go func() {
|
||
if err := server.Start(); err != nil {
|
||
logger.Fatalf("❌ Failed to start API server: %v", err)
|
||
}
|
||
}()
|
||
|
||
// Initialize and start Telegram bot (if configured)
|
||
var telegramBot *telegram.Bot
|
||
telegramConfig := telegram.LoadConfigFromEnv()
|
||
if telegramConfig.Token != "" {
|
||
logger.Info("🤖 Initializing Telegram AI Assistant...")
|
||
|
||
// Create AI client for the assistant
|
||
aiClient := createAssistantAIClient()
|
||
|
||
// Create AI Agent with trading tools
|
||
agentConfig := assistant.DefaultAgentConfig()
|
||
agent := assistant.NewAgent(aiClient, agentConfig)
|
||
|
||
// Register trading tools
|
||
tradingTools := assistant.NewTradingTools(traderManager, st)
|
||
agent.RegisterTools(tradingTools.GetAllTools()...)
|
||
|
||
// Set system prompt based on language
|
||
if telegramConfig.DefaultLanguage == "zh" {
|
||
agent.SetSystemPrompt(assistant.ChineseSystemPrompt())
|
||
}
|
||
|
||
// Create and start Telegram bot
|
||
var err error
|
||
telegramBot, err = telegram.NewBot(telegramConfig, agent)
|
||
if err != nil {
|
||
logger.Errorf("❌ Failed to create Telegram bot: %v", err)
|
||
} else {
|
||
go telegramBot.Start()
|
||
logger.Info("✅ Telegram AI Assistant started successfully")
|
||
}
|
||
} else {
|
||
logger.Info("ℹ️ Telegram bot not configured (set TELEGRAM_BOT_TOKEN to enable)")
|
||
}
|
||
|
||
// 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 Telegram bot
|
||
if telegramBot != nil {
|
||
telegramBot.Stop()
|
||
}
|
||
|
||
// Stop all traders
|
||
traderManager.StopAll()
|
||
logger.Info("✅ System shut down safely")
|
||
}
|
||
|
||
// newSharedMCPClient creates a shared MCP AI client (for backtesting)
|
||
func newSharedMCPClient() mcp.AIClient {
|
||
apiKey := os.Getenv("DEEPSEEK_API_KEY")
|
||
if apiKey == "" {
|
||
logger.Warn("⚠️ DEEPSEEK_API_KEY not set, AI features will be unavailable")
|
||
return nil
|
||
}
|
||
return mcp.NewDeepSeekClient()
|
||
}
|
||
|
||
// createAssistantAIClient creates an AI client for the Telegram assistant
|
||
// Supports multiple providers based on environment configuration
|
||
func createAssistantAIClient() mcp.AIClient {
|
||
// Try different providers in order of preference
|
||
|
||
// 1. DeepSeek (cost-effective, recommended)
|
||
if apiKey := os.Getenv("DEEPSEEK_API_KEY"); apiKey != "" {
|
||
client := mcp.NewDeepSeekClient()
|
||
customURL := os.Getenv("DEEPSEEK_API_URL")
|
||
customModel := os.Getenv("DEEPSEEK_MODEL")
|
||
client.SetAPIKey(apiKey, customURL, customModel)
|
||
logger.Info("🧠 Assistant using DeepSeek AI")
|
||
return client
|
||
}
|
||
|
||
// 2. Claude
|
||
if apiKey := os.Getenv("CLAUDE_API_KEY"); apiKey != "" {
|
||
client := mcp.NewClaudeClient()
|
||
customURL := os.Getenv("CLAUDE_API_URL")
|
||
customModel := os.Getenv("CLAUDE_MODEL")
|
||
client.SetAPIKey(apiKey, customURL, customModel)
|
||
logger.Info("🧠 Assistant using Claude AI")
|
||
return client
|
||
}
|
||
|
||
// 3. OpenAI
|
||
if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
|
||
client := mcp.NewOpenAIClient()
|
||
customURL := os.Getenv("OPENAI_API_URL")
|
||
customModel := os.Getenv("OPENAI_MODEL")
|
||
client.SetAPIKey(apiKey, customURL, customModel)
|
||
logger.Info("🧠 Assistant using OpenAI")
|
||
return client
|
||
}
|
||
|
||
// 4. Qwen
|
||
if apiKey := os.Getenv("QWEN_API_KEY"); apiKey != "" {
|
||
client := mcp.NewQwenClient()
|
||
customURL := os.Getenv("QWEN_API_URL")
|
||
customModel := os.Getenv("QWEN_MODEL")
|
||
client.SetAPIKey(apiKey, customURL, customModel)
|
||
logger.Info("🧠 Assistant using Qwen AI")
|
||
return client
|
||
}
|
||
|
||
logger.Warn("⚠️ No AI API key configured for assistant")
|
||
return nil
|
||
}
|
||
|
||
// 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
|
||
experience.SetInstallationID(installationID)
|
||
}
|