// Package agent implements the NOFXi Agent Core. // // The Agent is the central orchestrator that: // 1. Receives user messages (via Interaction layer) // 2. Routes intents (trade, query, analyze, chat) // 3. Uses Thinking layer for AI decisions // 4. Stores context in Memory layer // 5. Executes trades via Execution bridge (โ†’ NOFX engine) // 6. Monitors markets via Perception layer package agent import ( "context" "fmt" "log/slog" "strconv" "strings" "time" "github.com/NoFxAiOS/nofx/nofxi/internal/execution" "github.com/NoFxAiOS/nofx/nofxi/internal/memory" "github.com/NoFxAiOS/nofx/nofxi/internal/perception" "github.com/NoFxAiOS/nofx/nofxi/internal/thinking" ) // Agent is the NOFXi agent core. type Agent struct { config *Config memory *memory.Store thinker thinking.Engine bridge *execution.Bridge monitor *perception.MarketMonitor logger *slog.Logger // NotifyFunc sends proactive notifications to users (set by interaction layer) NotifyFunc func(userID int64, text string) error } // New creates a new Agent with the given config. func New(cfg *Config, mem *memory.Store, thinker thinking.Engine, logger *slog.Logger) *Agent { a := &Agent{ config: cfg, memory: mem, thinker: thinker, logger: logger, } return a } // SetBridge attaches the execution bridge. func (a *Agent) SetBridge(bridge *execution.Bridge) { a.bridge = bridge } // SetMonitor attaches the market monitor and wires up alert notifications. func (a *Agent) SetMonitor(monitor *perception.MarketMonitor) { a.monitor = monitor } // HandleMessage processes a user message and returns a response. func (a *Agent) HandleMessage(ctx context.Context, userID int64, text string) (string, error) { a.logger.Info("incoming message", "user_id", userID, "text", text) // Save user message to memory if err := a.memory.SaveMessage(userID, "user", text); err != nil { a.logger.Error("save user message", "error", err) } // Route intent intent := Route(text) a.logger.Info("routed intent", "type", intent.Type, "params", intent.Params) var response string var err error switch intent.Type { case IntentHelp: response = a.handleHelp() case IntentStatus: response = a.handleStatus() case IntentQuery: response, err = a.handleQuery(ctx, intent) case IntentAnalyze: response, err = a.handleAnalyze(ctx, intent) case IntentTrade: response, err = a.handleTrade(ctx, userID, intent) case IntentWatch: response = a.HandleWatchCommand(intent.Raw) case IntentSettings: response = a.handleSettings(intent) default: response, err = a.handleChat(ctx, userID, text) } if err != nil { a.logger.Error("handle message", "intent", intent.Type, "error", err) response = fmt.Sprintf("โš ๏ธ Error: %v", err) } // Save assistant response to memory if err := a.memory.SaveMessage(userID, "assistant", response); err != nil { a.logger.Error("save assistant message", "error", err) } return response, nil } func (a *Agent) handleHelp() string { return `๐Ÿค– *NOFXi โ€” Your AI Trading Agent* *Trading:* /buy BTC 0.01 โ€” Open long (market order) /sell BTC 0.01 โ€” Open short /close BTC โ€” Close position /positions โ€” View open positions /balance โ€” Check balance /pnl โ€” Profit & Loss history *Analysis:* /analyze BTC โ€” AI market analysis /watch BTC โ€” Monitor price /alert BTC above 100000 โ€” Price alert *System:* /status โ€” Agent status /settings โ€” Preferences /help โ€” This menu *Natural Language:* โ€ข "ๅธฎๆˆ‘ๅšๅคš ETH๏ผŒ2x ๆ ๆ†๏ผŒ0.1 ไธช" โ€ข "ๅˆ†ๆžไธ€ไธ‹ BTC ่ตฐๅŠฟ" โ€ข "็ŽฐๅœจๆŒไป“ๆƒ…ๅ†ตๆ€Žๆ ท" โ€ข "BTC ๅˆฐ 10 ไธ‡ๆ้†’ๆˆ‘" Just talk to me ๐Ÿ’ฌ` } func (a *Agent) handleStatus() string { // Market monitor status watchCount := 0 if a.monitor != nil { watchCount = len(a.monitor.GetAllSnapshots()) } bridgeStatus := "โŒ Not connected" if a.bridge != nil { bridgeStatus = "โœ… Connected" } return fmt.Sprintf(`๐Ÿ“Š *NOFXi Status* โ€ข Agent: %s โ€ข Model: %s โ€ข Provider: %s โ€ข Memory: โœ… Online โ€ข Execution: %s โ€ข Watching: %d symbols โ€ข Time: %s`, a.config.Agent.Name, a.config.LLM.Model, a.config.LLM.Provider, bridgeStatus, watchCount, time.Now().Format("2006-01-02 15:04:05"), ) } func (a *Agent) handleQuery(ctx context.Context, intent Intent) (string, error) { raw := strings.ToLower(intent.Raw) // Try live positions from exchange first if a.bridge != nil && (strings.Contains(raw, "position") || strings.Contains(raw, "ๆŒไป“")) { return a.queryPositions() } if a.bridge != nil && (strings.Contains(raw, "balance") || strings.Contains(raw, "ไฝ™้ข")) { return a.queryBalance() } // Fall back to trade history from memory trades, err := a.memory.GetRecentTrades(10) if err != nil { return "", fmt.Errorf("get trades: %w", err) } if len(trades) == 0 { return "๐Ÿ“ญ No trades yet. Start with `/buy BTC 0.01` or ask me to `/analyze BTC`.", nil } var sb strings.Builder sb.WriteString("๐Ÿ“‹ *Recent Trades*\n\n") totalPnL := 0.0 for _, t := range trades { emoji := "๐ŸŸข" if t.PnL < 0 { emoji = "๐Ÿ”ด" } sb.WriteString(fmt.Sprintf("%s %s %s %s โ€” $%.2f (P/L: $%.2f)\n", emoji, t.Side, t.Symbol, t.Exchange, t.Price*t.Quantity, t.PnL)) totalPnL += t.PnL } sb.WriteString(fmt.Sprintf("\n๐Ÿ’ฐ Total P/L: $%.2f", totalPnL)) return sb.String(), nil } func (a *Agent) queryPositions() (string, error) { var allPositions []execution.Position for _, ex := range a.config.Exchanges { positions, err := a.bridge.GetPositions(ex.Name) if err != nil { a.logger.Error("get positions", "exchange", ex.Name, "error", err) continue } allPositions = append(allPositions, positions...) } if len(allPositions) == 0 { return "๐Ÿ“ญ No open positions.", nil } var sb strings.Builder sb.WriteString("๐Ÿ“Š *Open Positions*\n\n") totalPnL := 0.0 for _, p := range allPositions { emoji := "๐ŸŸข" if p.PnL < 0 { emoji = "๐Ÿ”ด" } sb.WriteString(fmt.Sprintf("%s *%s* %s\n", emoji, p.Symbol, strings.ToUpper(p.Side))) sb.WriteString(fmt.Sprintf(" Size: %.4f | Entry: $%.2f\n", p.Size, p.EntryPrice)) sb.WriteString(fmt.Sprintf(" Mark: $%.2f | P/L: $%.2f\n", p.MarkPrice, p.PnL)) if p.Leverage > 0 { sb.WriteString(fmt.Sprintf(" Leverage: %.0fx | Exchange: %s\n", p.Leverage, p.Exchange)) } sb.WriteString("\n") totalPnL += p.PnL } sb.WriteString(fmt.Sprintf("๐Ÿ’ฐ *Total Unrealized P/L: $%.2f*", totalPnL)) return sb.String(), nil } func (a *Agent) queryBalance() (string, error) { var sb strings.Builder sb.WriteString("๐Ÿ’ฐ *Account Balance*\n\n") for _, ex := range a.config.Exchanges { bal, err := a.bridge.GetBalance(ex.Name) if err != nil { a.logger.Error("get balance", "exchange", ex.Name, "error", err) sb.WriteString(fmt.Sprintf("โ€ข %s: โš ๏ธ Error\n", ex.Name)) continue } sb.WriteString(fmt.Sprintf("*%s*\n", strings.Title(ex.Name))) sb.WriteString(fmt.Sprintf(" Total: $%.2f\n", bal.Total)) sb.WriteString(fmt.Sprintf(" Available: $%.2f\n", bal.Available)) sb.WriteString(fmt.Sprintf(" In Position: $%.2f\n\n", bal.InPosition)) } return sb.String(), nil } func (a *Agent) handleAnalyze(ctx context.Context, intent Intent) (string, error) { symbol := "BTC" if detail, ok := intent.Params["detail"]; ok && detail != "" { symbol = strings.ToUpper(strings.TrimSpace(detail)) } // Add live price if available priceInfo := "" if a.monitor != nil { if snap, ok := a.monitor.GetSnapshot(symbol + "USDT"); ok && snap.LastPrice > 0 { priceInfo = fmt.Sprintf("\nCurrent price: $%.2f", snap.LastPrice) } } if priceInfo == "" && a.bridge != nil { for _, ex := range a.config.Exchanges { if price, err := a.bridge.GetPrice(ex.Name, symbol+"USDT"); err == nil && price > 0 { priceInfo = fmt.Sprintf("\nCurrent price: $%.2f (from %s)", price, ex.Name) break } } } prompt := fmt.Sprintf( "Analyze %s/USDT for trading. %s\n"+ "Consider: trend, support/resistance, momentum indicators, volume, sentiment.\n"+ "Give specific entry/exit levels and stop loss. Be concise.", symbol, priceInfo) analysis, err := a.thinker.Analyze(ctx, prompt) if err != nil { return "", fmt.Errorf("AI analyze: %w", err) } emoji := map[string]string{ "buy": "๐ŸŸข BUY", "sell": "๐Ÿ”ด SELL", "hold": "๐ŸŸก HOLD", "wait": "โณ WAIT", } action := emoji[analysis.Action] if action == "" { action = "๐Ÿค” " + analysis.Action } result := fmt.Sprintf("๐Ÿ” *%s/USDT Analysis*\n\nSignal: %s\nConfidence: %.0f%%\n\n%s", symbol, action, analysis.Confidence*100, analysis.Reasoning) if analysis.StopLoss > 0 { result += fmt.Sprintf("\n\n๐Ÿ›‘ Stop Loss: $%.2f", analysis.StopLoss) } if analysis.TakeProfit > 0 { result += fmt.Sprintf("\n๐ŸŽฏ Take Profit: $%.2f", analysis.TakeProfit) } return result, nil } func (a *Agent) handleTrade(ctx context.Context, userID int64, intent Intent) (string, error) { action := strings.ToLower(intent.Params["action"]) detail := intent.Params["detail"] if a.bridge == nil { return fmt.Sprintf("โšก *Trade: %s %s*\n\n๐Ÿ”ง No exchange connected. Configure exchanges in config.yaml first.", strings.ToUpper(action), detail), nil } // Parse: "BTC 0.01" or "BTCUSDT 0.01 2x" parts := strings.Fields(detail) if len(parts) < 1 { return "โ“ Usage: `/buy BTC 0.01` or `/sell ETH 0.5 3x`", nil } symbol := strings.ToUpper(parts[0]) if !strings.HasSuffix(symbol, "USDT") { symbol += "USDT" } quantity := 0.0 leverage := 1 if len(parts) >= 2 { q, err := strconv.ParseFloat(parts[1], 64) if err != nil { return fmt.Sprintf("โ“ Invalid quantity: %s", parts[1]), nil } quantity = q } if len(parts) >= 3 { levStr := strings.TrimSuffix(strings.ToLower(parts[2]), "x") l, err := strconv.Atoi(levStr) if err == nil { leverage = l } } if quantity <= 0 && (action == "buy" || action == "sell" || action == "long" || action == "short") { return "โ“ Please specify quantity: `/buy BTC 0.01`", nil } // Map action to execution side var side string switch action { case "buy", "long", "open_long": side = "LONG" case "sell", "short", "open_short": side = "SHORT" case "close": side = "CLOSE_LONG" // TODO: detect which side to close default: side = strings.ToUpper(action) } // Use first configured exchange exchange := a.config.Exchanges[0].Name // Confirm with user before executing confirmMsg := fmt.Sprintf("โšก *Confirm Trade*\n\n"+ "โ€ข Action: %s\n"+ "โ€ข Symbol: %s\n"+ "โ€ข Quantity: %.6f\n"+ "โ€ข Leverage: %dx\n"+ "โ€ข Exchange: %s\n\n"+ "Reply 'yes' to execute.", side, symbol, quantity, leverage, exchange) // Store pending trade for confirmation a.memory.SetPreference(userID, "pending_trade", fmt.Sprintf("%s|%s|%f|%d|%s", side, symbol, quantity, leverage, exchange)) return confirmMsg, nil } // ExecutePendingTrade executes a trade that was previously confirmed. func (a *Agent) ExecutePendingTrade(ctx context.Context, userID int64) (string, error) { pending, err := a.memory.GetPreference(userID, "pending_trade") if err != nil || pending == "" { return "", fmt.Errorf("no pending trade") } // Clear pending a.memory.SetPreference(userID, "pending_trade", "") parts := strings.Split(pending, "|") if len(parts) != 5 { return "", fmt.Errorf("invalid pending trade data") } side := parts[0] symbol := parts[1] quantity, _ := strconv.ParseFloat(parts[2], 64) leverage, _ := strconv.Atoi(parts[3]) exchange := parts[4] result, err := a.bridge.PlaceOrder(exchange, symbol, side, quantity, leverage) if err != nil { return "", fmt.Errorf("execute trade: %w", err) } // Save trade to memory tradeRecord := &memory.TradeRecord{ Exchange: exchange, Symbol: symbol, Side: strings.ToLower(side), Type: "market", Quantity: quantity, Status: "open", } if price, ok := result["avgPrice"].(float64); ok { tradeRecord.Price = price } a.memory.SaveTrade(tradeRecord) return fmt.Sprintf("โœ… *Trade Executed!*\n\n"+ "โ€ข %s %s\n"+ "โ€ข Qty: %.6f\n"+ "โ€ข Leverage: %dx\n"+ "โ€ข Exchange: %s\n"+ "โ€ข Result: %v", side, symbol, quantity, leverage, exchange, result), nil } func (a *Agent) handleSettings(intent Intent) string { return `โš™๏ธ *Settings* โ€ข Language: ` + a.config.Agent.Language + ` โ€ข Model: ` + a.config.LLM.Model + ` โ€ข Provider: ` + a.config.LLM.Provider + ` โ€ข Exchanges: ` + fmt.Sprintf("%d configured", len(a.config.Exchanges)) } func (a *Agent) handleChat(ctx context.Context, userID int64, text string) (string, error) { // Check for trade confirmation lower := strings.ToLower(text) if lower == "yes" || lower == "y" || lower == "็กฎ่ฎค" || lower == "ๆ˜ฏ" { pending, _ := a.memory.GetPreference(userID, "pending_trade") if pending != "" { return a.ExecutePendingTrade(ctx, userID) } } // Get conversation history for context history, err := a.memory.GetRecentMessages(userID, 20) if err != nil { a.logger.Error("get history", "error", err) } // Build messages with system prompt messages := []thinking.Message{ { Role: "system", Content: fmt.Sprintf(`You are NOFXi, an AI trading agent built on NOFX. Your capabilities: - Market analysis and trading recommendations - Real-time position and balance monitoring - Trade execution (open/close positions on exchanges) - Price alerts and market monitoring - Risk management advice You support multiple exchanges: Binance, OKX, Bybit, Bitget, KuCoin, Gate, Hyperliquid, etc. Be concise, confident, and action-oriented. Use trading emojis. Respond in the same language the user uses. Current time: %s`, time.Now().Format("2006-01-02 15:04:05")), }, } for _, msg := range history { messages = append(messages, thinking.Message{ Role: msg.Role, Content: msg.Content, }) } messages = append(messages, thinking.Message{ Role: "user", Content: text, }) return a.thinker.Chat(ctx, messages) }