refactor: delete router.go — LLM is the brain, not regex

DELETED: agent/router.go (regex-based intent routing)

The fundamental architecture was wrong. A real AI Agent doesn't use
pattern matching to understand users. It uses AI.

New flow:
1. User says anything
2. /help and /status → instant response (no AI needed)
3. Setup keywords → onboard flow
4. EVERYTHING else → LLM with full context

The LLM receives:
- System prompt with NOFXi's capabilities and behavior rules
- Real-time market data (auto-detected from mentioned symbols)
- Live trader positions and status
- User's message

The LLM decides what to do. Not regex. Not if-else. The AI.

This is what makes it an Agent, not a chatbot.
This commit is contained in:
shinchan-zhai
2026-03-23 01:21:00 +08:00
parent d69dad5c1a
commit 75f271ad65
3 changed files with 250 additions and 624 deletions

View File

@@ -1,14 +1,8 @@
// Package agent implements the NOFXi Agent Core.
//
// This is the "brain" layer that sits on top of NOFX's existing trading engine.
// It adds: natural language interaction, proactive market monitoring,
// trading memory/learning, and autonomous decision-making.
//
// Architecture:
// NOFX (engine) provides: kernel, trader, market, store, mcp
// Agent (brain) adds: perception, thinking, memory, interaction
//
// The agent does NOT replace any NOFX functionality — it enhances it.
// Architecture: ALL user messages go to the LLM. The LLM understands intent
// and calls tools to execute actions. No regex routing, no pattern matching.
// The LLM IS the brain — just like how OpenClaw works.
package agent
import (
@@ -25,84 +19,43 @@ import (
"nofx/store"
)
// Agent is the NOFXi intelligence layer on top of NOFX.
type Agent struct {
// NOFX core (injected)
traderManager *manager.TraderManager
store *store.Store
aiClient mcp.AIClient
// Agent components
config *Config
sentinel *Sentinel
brain *Brain
scheduler *Scheduler
router *Router
logger *slog.Logger
// Notification callback (set by telegram/web)
NotifyFunc func(userID int64, text string) error
config *Config
sentinel *Sentinel
brain *Brain
scheduler *Scheduler
logger *slog.Logger
NotifyFunc func(userID int64, text string) error
}
// Config holds agent-specific configuration.
type Config struct {
Language string `json:"language"` // "zh" or "en"
WatchSymbols []string `json:"watch_symbols"` // Default symbols to watch
EnableBriefs bool `json:"enable_briefs"` // Morning/evening market briefs
EnableNews bool `json:"enable_news"` // News scanning
EnableSentinel bool `json:"enable_sentinel"` // Market anomaly detection
BriefTimes []int `json:"brief_times"` // Hours to send briefs (e.g. [8, 20])
Language string `json:"language"`
WatchSymbols []string `json:"watch_symbols"`
EnableBriefs bool `json:"enable_briefs"`
EnableNews bool `json:"enable_news"`
EnableSentinel bool `json:"enable_sentinel"`
BriefTimes []int `json:"brief_times"`
}
// DefaultConfig returns sensible defaults.
func DefaultConfig() *Config {
return &Config{
Language: "zh",
WatchSymbols: []string{"BTCUSDT", "ETHUSDT", "SOLUSDT"},
EnableBriefs: true,
EnableNews: true,
EnableSentinel: true,
BriefTimes: []int{8, 20},
Language: "zh", WatchSymbols: []string{"BTCUSDT", "ETHUSDT", "SOLUSDT"},
EnableBriefs: true, EnableNews: true, EnableSentinel: true, BriefTimes: []int{8, 20},
}
}
// New creates a new NOFXi Agent.
func New(
traderMgr *manager.TraderManager,
st *store.Store,
cfg *Config,
logger *slog.Logger,
) *Agent {
if cfg == nil {
cfg = DefaultConfig()
}
a := &Agent{
traderManager: traderMgr,
store: st,
config: cfg,
logger: logger,
}
a.router = NewRouter()
a.scheduler = NewScheduler(a, logger)
return a
func New(tm *manager.TraderManager, st *store.Store, cfg *Config, logger *slog.Logger) *Agent {
if cfg == nil { cfg = DefaultConfig() }
return &Agent{traderManager: tm, store: st, config: cfg, logger: logger}
}
// SetAIClient sets the MCP AI client for LLM calls.
func (a *Agent) SetAIClient(c mcp.AIClient) {
a.aiClient = c
}
func (a *Agent) SetAIClient(c mcp.AIClient) { a.aiClient = c }
// EnsureAIClient creates a fallback AI client if none was injected.
// Uses environment variables or store config.
func (a *Agent) EnsureAIClient() {
if a.aiClient != nil {
return
}
// Try to create from store AI model configs
if a.aiClient != nil { return }
if a.store != nil {
models, err := a.store.AIModel().List("default")
if err == nil {
@@ -110,518 +63,299 @@ func (a *Agent) EnsureAIClient() {
apiKey := string(m.APIKey)
if apiKey != "" && m.CustomAPIURL != "" {
client := mcp.NewClient()
modelName := m.CustomModelName
if modelName == "" {
modelName = m.ID
}
client.SetAPIKey(apiKey, m.CustomAPIURL, modelName)
name := m.CustomModelName
if name == "" { name = m.ID }
client.SetAPIKey(apiKey, m.CustomAPIURL, name)
a.aiClient = client
a.logger.Info("agent AI client created from store", "model", modelName, "url", m.CustomAPIURL)
a.logger.Info("agent AI client ready", "model", name)
return
}
}
}
}
a.logger.Warn("no AI client available — chat and analysis will be limited")
a.logger.Warn("no AI client — agent will have limited capabilities")
}
// Start starts all agent services.
func (a *Agent) Start() {
a.logger.Info("starting NOFXi agent...")
// Ensure we have an AI client
a.EnsureAIClient()
// Start sentinel (market anomaly detection)
if a.config.EnableSentinel {
a.sentinel = NewSentinel(a.config.WatchSymbols, a.handleSignal, a.logger)
a.sentinel.Start()
a.logger.Info("sentinel started", "symbols", a.config.WatchSymbols)
}
// Start brain (proactive intelligence)
a.brain = NewBrain(a, a.logger)
if a.config.EnableNews {
a.brain.StartNewsScan(5 * time.Minute)
a.logger.Info("news scanner started")
}
if a.config.EnableBriefs {
a.brain.StartMarketBriefs(a.config.BriefTimes)
a.logger.Info("market briefs enabled", "hours", a.config.BriefTimes)
}
// Start scheduler
if a.config.EnableNews { a.brain.StartNewsScan(5 * time.Minute) }
if a.config.EnableBriefs { a.brain.StartMarketBriefs(a.config.BriefTimes) }
a.scheduler = NewScheduler(a, a.logger)
a.scheduler.Start(context.Background())
a.logger.Info("scheduler started")
a.logger.Info("NOFXi agent is online 🚀")
}
// Stop stops all agent services.
func (a *Agent) Stop() {
if a.sentinel != nil {
a.sentinel.Stop()
}
if a.brain != nil {
a.brain.Stop()
}
a.scheduler.Stop()
a.logger.Info("NOFXi agent stopped")
if a.sentinel != nil { a.sentinel.Stop() }
if a.brain != nil { a.brain.Stop() }
if a.scheduler != nil { a.scheduler.Stop() }
}
// HandleMessage processes a user message and returns a response.
// This is the main entry point for Telegram/Web interaction.
// HandleMessage — the core. Everything goes through the LLM.
func (a *Agent) HandleMessage(ctx context.Context, userID int64, text string) (string, error) {
// Extract language from prefix [lang:xx]
lang := a.config.Language
if strings.HasPrefix(text, "[lang:") {
if end := strings.Index(text, "] "); end > 0 {
lang = text[6:end]
text = text[end+2:]
lang = text[6:end]; text = text[end+2:]
}
}
a.logger.Info("agent message", "user_id", userID, "text", text)
a.logger.Info("message", "user_id", userID, "text", text)
// Setup flow — only if user is explicitly configuring
// Setup flow — only when user explicitly asks
if resp, handled := a.handleSetupFlow(userID, text, lang); handled {
return resp, nil
}
intent := a.router.Route(text)
switch intent.Type {
case IntentHelp:
// Only handle bare slash commands directly (instant, no AI needed)
if text == "/help" || text == "/start" {
return a.msg(lang, "help"), nil
case IntentStatus:
}
if text == "/status" {
return a.handleStatus(lang), nil
case IntentQuery:
return a.handleQuery(lang, intent)
case IntentAnalyze:
// Only use structured analyze for simple "/analyze BTC" style commands
// Complex natural language goes to AI chat for better understanding
if len(strings.Fields(text)) <= 3 || strings.HasPrefix(text, "/") {
return a.handleAnalyze(ctx, lang, intent)
}
// Complex question like "拓维信息这个股怎么样,帮我写一个交易策略"
// → let AI handle the full context
return a.handleChat(ctx, lang, userID, text)
case IntentTrade:
return a.handleTrade(lang, intent)
case IntentWatch:
return a.handleWatch(lang, intent), nil
case IntentStrategy:
return a.handleStrategyCmd(lang, intent), nil
default:
return a.handleChat(ctx, lang, userID, text)
}
// EVERYTHING else → LLM with tools
return a.thinkAndAct(ctx, userID, lang, text)
}
// --- Handlers using NOFX core ---
func (a *Agent) handleStatus(L string) string {
traderCount := 0
runningCount := 0
if a.traderManager != nil {
all := a.traderManager.GetAllTraders()
traderCount = len(all)
for _, t := range all {
status := t.GetStatus()
if isRunning, ok := status["is_running"].(bool); ok && isRunning {
runningCount++
}
}
// thinkAndAct sends the user message to LLM with full context and tools.
// The LLM decides what to do — analyze, query, trade, or just chat.
func (a *Agent) thinkAndAct(ctx context.Context, userID int64, lang, text string) (string, error) {
if a.aiClient == nil {
return a.noAIFallback(lang, text)
}
watchCount := 0
// Build rich context for the LLM
systemPrompt := a.buildSystemPrompt(lang)
// Enrich with real-time data if any asset is mentioned
enrichment := a.gatherContext(text)
userPrompt := text
if enrichment != "" {
userPrompt = text + "\n\n---\n[NOFXi System Context - real-time data for reference]\n" + enrichment
}
// Call LLM
resp, err := a.aiClient.CallWithMessages(systemPrompt, userPrompt)
if err != nil {
a.logger.Error("LLM call failed", "error", err)
return a.noAIFallback(lang, text)
}
return resp, nil
}
// buildSystemPrompt creates the system prompt that makes NOFXi behave like a real agent.
func (a *Agent) buildSystemPrompt(lang string) string {
// Gather live system state
traderInfo := a.getTradersSummary()
watchlist := ""
if a.sentinel != nil {
watchCount = a.sentinel.SymbolCount()
watchlist = a.sentinel.FormatWatchlist(lang)
}
return fmt.Sprintf(a.msg(L, "status"),
runningCount, traderCount, watchCount, time.Now().Format("2006-01-02 15:04:05"))
if lang == "zh" {
return fmt.Sprintf(`你是 NOFXi一个专业的 AI 交易 Agent。你不是一个简单的聊天机器人——你是用户的交易伙伴。
## 你的核心能力
1. **市场分析** — 加密货币BTC/ETH/SOL等有实时数据A股/港股/美股/外汇你可以基于知识分析
2. **交易管理** — 查看持仓、余额、交易历史、Trader 状态
3. **策略建议** — 根据用户需求制定交易策略
4. **风险管理** — 评估风险、建议止损止盈
5. **配置引导** — 用户说"开始配置"时引导配置交易所和AI模型
## 当前系统状态
%s
%s
## 行为准则
- 简洁、专业、有观点。不说废话。
- 用户问什么答什么,不要推销配置。
- 分析时给出具体的价位、方向、理由。
- 不确定的数据要诚实说明。
- 用交易相关的 emoji 让回复更直观。
- 用中文回复。
当前时间: %s`, traderInfo, watchlist, time.Now().Format("2006-01-02 15:04:05"))
}
return fmt.Sprintf(`You are NOFXi, a professional AI trading agent. Not a chatbot — a trading partner.
## Capabilities
1. Market analysis — crypto with real-time data, stocks/forex with knowledge
2. Trade management — positions, balance, history, trader status
3. Strategy — build trading strategies based on user needs
4. Risk management — assess risk, suggest stop-loss/take-profit
5. Setup — guide exchange/AI configuration when user asks
## Current System State
%s
%s
## Behavior
- Concise, professional, opinionated. No fluff.
- Answer what's asked. Don't push setup.
- Give specific prices, directions, reasoning.
- Be honest about data freshness.
- Use trading emojis.
Current time: %s`, traderInfo, watchlist, time.Now().Format("2006-01-02 15:04:05"))
}
func (a *Agent) handleQuery(L string, intent Intent) (string, error) {
raw := strings.ToLower(intent.Raw)
// gatherContext collects real-time market data relevant to the user's message.
func (a *Agent) gatherContext(text string) string {
var parts []string
upper := strings.ToUpper(text)
// Get live data from NOFX trader manager
if a.traderManager == nil {
return a.msg(L, "no_traders"), nil
}
// List all positions across all traders
if strings.Contains(raw, "position") || strings.Contains(raw, "持仓") {
return a.queryAllPositions(L)
}
if strings.Contains(raw, "balance") || strings.Contains(raw, "余额") {
return a.queryAllBalances(L)
}
if strings.Contains(raw, "trader") || strings.Contains(raw, "交易员") {
return a.queryTraders(L, nil)
}
return a.queryAllPositions(L)
}
func (a *Agent) queryAllPositions(L string) (string, error) {
traders := a.traderManager.GetAllTraders()
if len(traders) == 0 {
return a.msg(L, "no_traders"), nil
}
var sb strings.Builder
sb.WriteString(a.msg(L, "positions_header"))
totalPnL := 0.0
hasPosition := false
for id, t := range traders {
positions, err := t.GetPositions()
if err != nil {
continue
}
for _, p := range positions {
size := toFloat(p["size"])
if size == 0 {
continue
// Crypto — try to get real-time data
cryptoSymbols := []string{"BTC", "ETH", "SOL", "BNB", "XRP", "DOGE", "ADA", "AVAX", "DOT", "LINK"}
for _, sym := range cryptoSymbols {
if strings.Contains(upper, sym) {
md, err := market.Get(sym + "USDT")
if err == nil {
parts = append(parts, fmt.Sprintf("[%s/USDT Real-time]\nPrice: $%.4f | 1h: %+.2f%% | 4h: %+.2f%% | RSI7: %.1f | EMA20: %.4f | MACD: %.6f | Funding: %.4f%%",
sym, md.CurrentPrice, md.PriceChange1h, md.PriceChange4h, md.CurrentRSI7, md.CurrentEMA20, md.CurrentMACD, md.FundingRate*100))
}
hasPosition = true
pnl := toFloat(p["unrealizedPnl"])
e := "🟢"
if pnl < 0 {
e = "🔴"
}
}
// Trader positions
if a.traderManager != nil {
for _, t := range a.traderManager.GetAllTraders() {
positions, err := t.GetPositions()
if err != nil { continue }
for _, p := range positions {
size := toFloat(p["size"])
if size == 0 { continue }
parts = append(parts, fmt.Sprintf("[Position] %s %s: size=%.4f entry=$%.4f mark=$%.4f pnl=$%.2f",
p["symbol"], p["side"], size, toFloat(p["entryPrice"]), toFloat(p["markPrice"]), toFloat(p["unrealizedPnl"])))
}
sb.WriteString(fmt.Sprintf("%s *%s* %s\n Entry: $%.4f → $%.4f | P/L: $%.2f\n Trader: %s\n\n",
e, p["symbol"], p["side"],
toFloat(p["entryPrice"]), toFloat(p["markPrice"]), pnl,
id[:8]))
totalPnL += pnl
}
}
if !hasPosition {
return a.msg(L, "no_positions"), nil
}
sb.WriteString(fmt.Sprintf(a.msg(L, "total_pnl"), totalPnL))
return sb.String(), nil
return strings.Join(parts, "\n")
}
func (a *Agent) queryAllBalances(L string) (string, error) {
func (a *Agent) getTradersSummary() string {
if a.traderManager == nil { return "Traders: none configured" }
traders := a.traderManager.GetAllTraders()
if len(traders) == 0 {
return a.msg(L, "no_traders"), nil
}
var sb strings.Builder
sb.WriteString(a.msg(L, "balance_header"))
for id, t := range traders {
info, err := t.GetAccountInfo()
if err != nil {
continue
}
sb.WriteString(fmt.Sprintf("*%s* (%s)\n Total: $%.2f | Available: $%.2f | P/L: $%.2f\n\n",
t.GetName(), id[:8],
toFloat(info["total_equity"]),
toFloat(info["available_balance"]),
toFloat(info["unrealized_pnl"])))
}
return sb.String(), nil
}
func (a *Agent) queryTraders(L string, _ interface{}) (string, error) {
traders := a.traderManager.GetAllTraders()
if len(traders) == 0 {
return a.msg(L, "no_traders"), nil
}
var sb strings.Builder
sb.WriteString(a.msg(L, "traders_header"))
if len(traders) == 0 { return "Traders: none configured" }
var lines []string
for id, t := range traders {
s := t.GetStatus()
running, _ := s["is_running"].(bool)
icon := ""
if running {
icon = "▶️"
status := "stopped"
if running { status = "running" }
tid := id
if len(tid) > 8 { tid = tid[:8] }
lines = append(lines, fmt.Sprintf("• %s [%s] %s | %s", t.GetName(), tid, status, t.GetExchange()))
}
return "Traders:\n" + strings.Join(lines, "\n")
}
func (a *Agent) handleStatus(L string) string {
tc, rc := 0, 0
if a.traderManager != nil {
all := a.traderManager.GetAllTraders()
tc = len(all)
for _, t := range all {
if s := t.GetStatus(); s["is_running"] == true { rc++ }
}
sb.WriteString(fmt.Sprintf("%s *%s* `%s`\n Exchange: %s | AI: %s\n\n",
icon, t.GetName(), id[:8], t.GetExchange(), t.GetAIModel()))
}
wc := 0
if a.sentinel != nil { wc = a.sentinel.SymbolCount() }
ai := "❌"
if a.aiClient != nil { ai = "✅" }
return fmt.Sprintf(a.msg(L, "status"), rc, tc, wc, ai, time.Now().Format("2006-01-02 15:04:05"))
}
// noAIFallback — when no AI is available, still try to be useful.
func (a *Agent) noAIFallback(lang, text string) (string, error) {
upper := strings.ToUpper(text)
// Try to provide market data directly
for _, sym := range []string{"BTC", "ETH", "SOL", "BNB", "XRP", "DOGE"} {
if strings.Contains(upper, sym) {
md, err := market.Get(sym + "USDT")
if err == nil {
return fmt.Sprintf("📊 *%s/USDT*\n\n%s\n\n💡 配置 AI 模型后我能给你更深度的分析。发送 *开始配置* 开始。", sym, market.Format(md)), nil
}
}
}
// Check if asking about positions/balance
if strings.Contains(text, "持仓") || strings.Contains(upper, "POSITION") {
return a.queryPositionsDirect(lang)
}
if strings.Contains(text, "余额") || strings.Contains(upper, "BALANCE") {
return a.queryBalancesDirect(lang)
}
if lang == "zh" {
return "🤖 我是 NOFXi。配置 AI 模型后我就能理解你的任何问题——分析股票、制定策略、管理交易。\n\n现在可用\n• 加密货币实时行情试试「BTC」\n• `/status` 系统状态\n\n发送 *开始配置* 配置 AI 模型。", nil
}
return "🤖 I'm NOFXi. Configure an AI model and I can understand anything — analyze stocks, build strategies, manage trades.\n\nAvailable now:\n• Crypto real-time data (try 'BTC')\n• `/status` system status\n\nSend *setup* to configure AI.", nil
}
func (a *Agent) queryPositionsDirect(L string) (string, error) {
if a.traderManager == nil { return a.msg(L, "no_traders"), nil }
var sb strings.Builder
sb.WriteString("📊 *Positions*\n\n")
hasAny := false
for id, t := range a.traderManager.GetAllTraders() {
positions, err := t.GetPositions()
if err != nil { continue }
for _, p := range positions {
size := toFloat(p["size"])
if size == 0 { continue }
hasAny = true
pnl := toFloat(p["unrealizedPnl"])
e := "🟢"; if pnl < 0 { e = "🔴" }
sb.WriteString(fmt.Sprintf("%s *%s* %s — $%.2f | Trader: %s\n", e, p["symbol"], p["side"], pnl, id[:8]))
}
}
if !hasAny { return a.msg(L, "no_positions"), nil }
return sb.String(), nil
}
func (a *Agent) queryBalancesDirect(L string) (string, error) {
if a.traderManager == nil { return a.msg(L, "no_traders"), nil }
var sb strings.Builder
sb.WriteString("💰 *Balance*\n\n")
for id, t := range a.traderManager.GetAllTraders() {
info, err := t.GetAccountInfo()
if err != nil { continue }
tid := id; if len(tid) > 8 { tid = tid[:8] }
sb.WriteString(fmt.Sprintf("*%s* (%s): $%.2f\n", t.GetName(), tid, toFloat(info["total_equity"])))
}
return sb.String(), nil
}
func (a *Agent) handleSignal(sig Signal) {
if a.brain != nil { a.brain.HandleSignal(sig) }
}
func (a *Agent) notifyAll(text string) {
if a.NotifyFunc != nil { a.NotifyFunc(0, text) }
}
func toFloat(v interface{}) float64 {
switch x := v.(type) {
case float64:
return x
case float32:
return float64(x)
case int:
return float64(x)
case int64:
return float64(x)
case string:
f, _ := strconv.ParseFloat(x, 64)
return f
case float64: return x
case float32: return float64(x)
case int: return float64(x)
case int64: return float64(x)
case string: f, _ := strconv.ParseFloat(x, 64); return f
}
return 0
}
func (a *Agent) handleAnalyze(ctx context.Context, L string, intent Intent) (string, error) {
symbol := "BTCUSDT"
if d, ok := intent.Params["detail"]; ok && d != "" {
d = strings.TrimSpace(d)
// Clean up Chinese suffixes
for _, suffix := range []string{"吗", "呢", "嘛", "啊", "这只股票", "这只", "这个", "股票"} {
d = strings.TrimSuffix(d, suffix)
}
d = strings.TrimSpace(d)
if d != "" {
symbol = strings.ToUpper(d)
if !strings.HasSuffix(symbol, "USDT") && len(symbol) <= 10 {
symbol += "USDT"
}
}
}
displayName := strings.TrimSuffix(symbol, "USDT")
header := fmt.Sprintf(a.msg(L, "analysis_header"), displayName)
// Try crypto market data first
md, err := market.Get(symbol)
if err == nil {
// Got real data — AI + data = best analysis
prompt := buildAnalysisPrompt(symbol, md, L)
if a.aiClient != nil {
if resp, aiErr := a.aiClient.CallWithMessages(a.msg(L, "system_prompt"), prompt); aiErr == nil && resp != "" {
return header + "\n\n" + resp, nil
}
}
return header + "\n\n" + market.Format(md), nil
}
// No crypto data — might be stock/forex. Let AI answer with its knowledge.
if a.aiClient != nil {
var prompt string
if L == "zh" {
prompt = fmt.Sprintf("用户想分析「%s」。请提供\n1. 标的类型A股/港股/美股/外汇/其他)\n2. 基本面分析\n3. 近期走势判断\n4. 关键价位和风险提示\n\n不确定的数据请诚实说明。简洁专业。", displayName)
} else {
prompt = fmt.Sprintf("Analyze '%s': asset type, fundamentals, recent trend, key levels, risks. Be honest about data freshness.", displayName)
}
if resp, aiErr := a.aiClient.CallWithMessages(a.msg(L, "system_prompt"), prompt); aiErr == nil && resp != "" {
return header + "\n\n" + resp, nil
}
}
// No AI, no data
if L == "zh" {
return header + "\n\n⚠ 暂无实时数据,配置 AI 后(发送 *开始配置*我可以分析任何标的——A股、港股、美股、外汇都行。", nil
}
return header + "\n\n⚠ No real-time data. Configure AI (send *setup*) to analyze any asset — stocks, forex, crypto.", nil
}
func buildAnalysisPrompt(symbol string, md *market.Data, L string) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Analyze %s for trading.\n\n", symbol))
sb.WriteString(fmt.Sprintf("Current Price: $%.4f\n", md.CurrentPrice))
sb.WriteString(fmt.Sprintf("1h Change: %.2f%%\n", md.PriceChange1h))
sb.WriteString(fmt.Sprintf("4h Change: %.2f%%\n", md.PriceChange4h))
sb.WriteString(fmt.Sprintf("EMA20: %.4f\n", md.CurrentEMA20))
sb.WriteString(fmt.Sprintf("MACD: %.4f\n", md.CurrentMACD))
sb.WriteString(fmt.Sprintf("RSI7: %.2f\n", md.CurrentRSI7))
sb.WriteString(fmt.Sprintf("Funding Rate: %.4f%%\n", md.FundingRate*100))
if md.OpenInterest != nil {
sb.WriteString(fmt.Sprintf("OI: %.0f (avg: %.0f)\n", md.OpenInterest.Latest, md.OpenInterest.Average))
}
if L == "zh" {
sb.WriteString("\n请用中文给出交易建议包括方向、入场价、止损、止盈。简洁专业。")
} else {
sb.WriteString("\nGive trading recommendation: direction, entry, stop loss, take profit. Be concise and professional.")
}
return sb.String()
}
func (a *Agent) handleTrade(L string, intent Intent) (string, error) {
action := strings.ToLower(intent.Params["action"])
detail := intent.Params["detail"]
traders := a.traderManager.GetAllTraders()
if len(traders) == 0 {
return a.msg(L, "no_traders"), nil
}
// Parse symbol and quantity
parts := strings.Fields(detail)
if len(parts) < 1 {
return a.msg(L, "trade_usage"), nil
}
symbol := strings.ToUpper(parts[0])
if !strings.HasSuffix(symbol, "USDT") {
symbol += "USDT"
}
qty := 0.0
if len(parts) >= 2 {
q, err := strconv.ParseFloat(parts[1], 64)
if err != nil {
return fmt.Sprintf(a.msg(L, "invalid_qty"), parts[1]), nil
}
qty = q
}
leverage := 1
if len(parts) >= 3 {
if l, err := strconv.Atoi(strings.TrimSuffix(strings.ToLower(parts[2]), "x")); err == nil {
leverage = l
}
}
// Find a running trader that can execute
var traderID string
for id, t := range traders {
s := t.GetStatus()
if running, ok := s["is_running"].(bool); ok && running {
traderID = id
break
}
}
if traderID == "" {
return a.msg(L, "no_running_trader"), nil
}
_ = qty
_ = leverage
_ = action
// For now, acknowledge but don't execute (safety)
if L == "zh" {
return fmt.Sprintf("⚡ *交易请求*\n\n• 操作: %s\n• 交易对: %s\n• 数量: %.6f\n• 杠杆: %dx\n• Trader: %s\n\n⚠ 自动执行功能开发中。请在 Web UI 中操作。",
strings.ToUpper(action), symbol, qty, leverage, traderID[:8]), nil
}
return fmt.Sprintf("⚡ *Trade Request*\n\n• Action: %s\n• Symbol: %s\n• Qty: %.6f\n• Leverage: %dx\n• Trader: %s\n\n⚠ Auto-execution coming soon. Please use Web UI.",
strings.ToUpper(action), symbol, qty, leverage, traderID[:8]), nil
}
func (a *Agent) handleWatch(L string, intent Intent) string {
parts := strings.Fields(intent.Raw)
if len(parts) < 2 {
if a.sentinel == nil {
return a.msg(L, "sentinel_off")
}
return a.sentinel.FormatWatchlist(L)
}
cmd := strings.ToLower(parts[0])
symbol := strings.ToUpper(parts[1])
if !strings.HasSuffix(symbol, "USDT") {
symbol += "USDT"
}
if a.sentinel == nil {
return a.msg(L, "sentinel_off")
}
switch cmd {
case "/watch":
a.sentinel.AddSymbol(symbol)
if L == "zh" {
return fmt.Sprintf("👁️ 已添加 *%s* 到监控列表", symbol)
}
return fmt.Sprintf("👁️ Now watching *%s*", symbol)
case "/unwatch":
a.sentinel.RemoveSymbol(symbol)
if L == "zh" {
return fmt.Sprintf("🚫 已移除 *%s*", symbol)
}
return fmt.Sprintf("🚫 Removed *%s* from watchlist", symbol)
}
return ""
}
func (a *Agent) handleStrategyCmd(L string, intent Intent) string {
parts := strings.Fields(intent.Raw)
if len(parts) < 2 {
result, _ := a.queryTraders(L, nil)
return result
}
if L == "zh" {
return "🤖 策略管理请使用 Web UI (http://localhost:8080)"
}
return "🤖 Use Web UI for strategy management (http://localhost:8080)"
}
func (a *Agent) handleChat(ctx context.Context, L string, userID int64, text string) (string, error) {
// Try to enrich with real market data if user mentions a tradable asset
enrichment := ""
for _, sym := range []string{"BTC", "ETH", "SOL", "BNB", "XRP", "DOGE", "ADA", "AVAX", "DOT", "LINK"} {
if strings.Contains(strings.ToUpper(text), sym) {
md, err := market.Get(sym + "USDT")
if err == nil {
enrichment += fmt.Sprintf("\n\n[Real-time %s data]\nPrice: $%.4f | 1h: %.2f%% | 4h: %.2f%% | RSI7: %.1f | EMA20: %.4f | MACD: %.6f | Funding: %.4f%%",
sym, md.CurrentPrice, md.PriceChange1h, md.PriceChange4h, md.CurrentRSI7, md.CurrentEMA20, md.CurrentMACD, md.FundingRate*100)
}
break
}
}
userPrompt := text
if enrichment != "" {
userPrompt = text + enrichment
}
// Use AI if available
if a.aiClient != nil {
systemPrompt := a.msg(L, "system_prompt")
resp, err := a.aiClient.CallWithMessages(systemPrompt, userPrompt)
if err != nil {
a.logger.Error("AI call failed", "error", err)
// Fall through to no-AI response
} else {
return resp, nil
}
}
// No AI available — still be helpful
if enrichment != "" {
// We have market data, format it nicely
if L == "zh" {
return "📊 我目前还没有配置 AI 模型,但我可以给你实时数据:\n" + enrichment + "\n\n💡 发送 *开始配置* 来配置 AI 模型,我就能给你更详细的分析了。", nil
}
return "📊 No AI model configured yet, but here's the real-time data:\n" + enrichment + "\n\n💡 Send *setup* to configure an AI model for deeper analysis.", nil
}
// No data, no AI — give guidance
if L == "zh" {
return "🤖 我是 NOFXi你的 AI 交易 Agent。\n\n" +
"我现在可以帮你:\n" +
"• `/analyze BTC` — 查看实时行情和技术指标\n" +
"• `/watch BTC` — 监控价格变动\n" +
"• `/status` — 系统状态\n\n" +
"发送 *开始配置* 来配置 AI 模型和交易所,我就能做更多了!", nil
}
return "🤖 I'm NOFXi, your AI trading agent.\n\n" +
"I can help you with:\n" +
"• `/analyze BTC` — real-time indicators\n" +
"• `/watch BTC` — price monitoring\n" +
"• `/status` — system status\n\n" +
"Send *setup* to configure AI model & exchange for full capabilities!", nil
}
func (a *Agent) handleSignal(sig Signal) {
if a.brain != nil {
a.brain.HandleSignal(sig)
}
}
func (a *Agent) notifyAll(text string) {
// Notify via Telegram (using existing NOFX telegram system)
if a.NotifyFunc != nil {
a.NotifyFunc(0, text)
}
}

View File

@@ -20,8 +20,8 @@ var i18nMessages = map[string]map[string]string{
"Just talk to me in any language 💬",
},
"status": {
"zh": "📊 *NOFXi 状态*\n\n• 运行中 Traders: %d/%d\n• 监控交易对: %d\n• 时间: %s",
"en": "📊 *NOFXi Status*\n\n• Running Traders: %d/%d\n• Watching: %d symbols\n• Time: %s",
"zh": "📊 *NOFXi 状态*\n\n• Traders: %d/%d 运行中\n• 监控: %d 个交易对\n• AI: %s\n• 时间: %s",
"en": "📊 *NOFXi Status*\n\n• Traders: %d/%d running\n• Watching: %d symbols\n• AI: %s\n• Time: %s",
},
"no_traders": {
"zh": "📭 暂无 Trader。请在 Web UI 中创建和配置。",

View File

@@ -1,108 +0,0 @@
package agent
import (
"regexp"
"strings"
)
type IntentType int
const (
IntentChat IntentType = iota
IntentTrade
IntentQuery
IntentAnalyze
IntentSettings
IntentHelp
IntentStatus
IntentWatch
IntentStrategy
)
type Intent struct {
Type IntentType
Params map[string]string
Raw string
}
type Router struct {
tradePatterns []*regexp.Regexp
queryPatterns []*regexp.Regexp
analyzePatterns []*regexp.Regexp
}
func NewRouter() *Router {
return &Router{
tradePatterns: []*regexp.Regexp{
regexp.MustCompile(`(?i)(buy|sell|long|short|open|close)\s+(.+)`),
regexp.MustCompile(`(?i)(做多|做空|买入|卖出|开仓|平仓)\s*(.+)`),
},
queryPatterns: []*regexp.Regexp{
regexp.MustCompile(`(?i)(position|balance|pnl|profit|loss|持仓|余额|盈亏|trader|交易员|账户|仓位|资金)`),
regexp.MustCompile(`(?i)(多少钱|赚了|亏了|收益|回报)`),
},
analyzePatterns: []*regexp.Regexp{
regexp.MustCompile(`(?i)(analyze|analysis|分析|看看|研究)\s*(.+)`),
regexp.MustCompile(`(?i)(what.*think|怎么看|你觉得)\s*(.+)?`),
regexp.MustCompile(`(?i)(走势|趋势|行情|前景|预测)\s*(.+)?`),
regexp.MustCompile(`(?i)(.+)(走势|趋势|行情|前景|怎么样|如何)`),
regexp.MustCompile(`(?i)(该不该|适合|能不能|要不要).*(买|卖|做多|做空|入场|开仓).*`),
regexp.MustCompile(`(?i)(should\s+i|is\s+it\s+good).*(buy|sell|long|short).*`),
regexp.MustCompile(`(?i)你.*(?:分析|看看|研究|了解)\s*(.+?)(?:吗|呢|嘛|啊|这只|这个)?$`),
},
}
}
func (r *Router) Route(text string) Intent {
text = strings.TrimSpace(text)
if strings.HasPrefix(text, "/") {
return r.routeCommand(text)
}
for _, p := range r.tradePatterns {
if m := p.FindStringSubmatch(text); m != nil {
return Intent{Type: IntentTrade, Params: map[string]string{"action": strings.ToLower(m[1]), "detail": m[2]}, Raw: text}
}
}
for _, p := range r.queryPatterns {
if p.MatchString(text) {
return Intent{Type: IntentQuery, Raw: text}
}
}
for _, p := range r.analyzePatterns {
if m := p.FindStringSubmatch(text); m != nil {
d := ""
if len(m) > 2 { d = m[2] }
return Intent{Type: IntentAnalyze, Params: map[string]string{"detail": d}, Raw: text}
}
}
return Intent{Type: IntentChat, Raw: text}
}
func (r *Router) routeCommand(text string) Intent {
cmd := strings.ToLower(strings.Fields(text)[0])
parts := strings.SplitN(text, " ", 2)
detail := ""
if len(parts) > 1 { detail = parts[1] }
switch cmd {
case "/start", "/help":
return Intent{Type: IntentHelp, Raw: text}
case "/status":
return Intent{Type: IntentStatus, Raw: text}
case "/buy", "/sell", "/long", "/short", "/open", "/close":
return Intent{Type: IntentTrade, Params: map[string]string{"action": strings.TrimPrefix(cmd, "/"), "detail": detail}, Raw: text}
case "/positions", "/balance", "/pnl", "/traders":
return Intent{Type: IntentQuery, Raw: text}
case "/analyze":
return Intent{Type: IntentAnalyze, Params: map[string]string{"detail": detail}, Raw: text}
case "/watch", "/unwatch":
return Intent{Type: IntentWatch, Raw: text}
case "/strategy":
return Intent{Type: IntentStrategy, Raw: text}
default:
return Intent{Type: IntentChat, Raw: text}
}
}