mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-10 22:36:58 +08:00
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:
762
agent/agent.go
762
agent/agent.go
@@ -1,14 +1,8 @@
|
|||||||
// Package agent implements the NOFXi Agent Core.
|
// Package agent implements the NOFXi Agent Core.
|
||||||
//
|
//
|
||||||
// This is the "brain" layer that sits on top of NOFX's existing trading engine.
|
// Architecture: ALL user messages go to the LLM. The LLM understands intent
|
||||||
// It adds: natural language interaction, proactive market monitoring,
|
// and calls tools to execute actions. No regex routing, no pattern matching.
|
||||||
// trading memory/learning, and autonomous decision-making.
|
// The LLM IS the brain — just like how OpenClaw works.
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
package agent
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -25,84 +19,43 @@ import (
|
|||||||
"nofx/store"
|
"nofx/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Agent is the NOFXi intelligence layer on top of NOFX.
|
|
||||||
type Agent struct {
|
type Agent struct {
|
||||||
// NOFX core (injected)
|
|
||||||
traderManager *manager.TraderManager
|
traderManager *manager.TraderManager
|
||||||
store *store.Store
|
store *store.Store
|
||||||
aiClient mcp.AIClient
|
aiClient mcp.AIClient
|
||||||
|
config *Config
|
||||||
// Agent components
|
sentinel *Sentinel
|
||||||
config *Config
|
brain *Brain
|
||||||
sentinel *Sentinel
|
scheduler *Scheduler
|
||||||
brain *Brain
|
logger *slog.Logger
|
||||||
scheduler *Scheduler
|
NotifyFunc func(userID int64, text string) error
|
||||||
router *Router
|
|
||||||
logger *slog.Logger
|
|
||||||
|
|
||||||
// Notification callback (set by telegram/web)
|
|
||||||
NotifyFunc func(userID int64, text string) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config holds agent-specific configuration.
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Language string `json:"language"` // "zh" or "en"
|
Language string `json:"language"`
|
||||||
WatchSymbols []string `json:"watch_symbols"` // Default symbols to watch
|
WatchSymbols []string `json:"watch_symbols"`
|
||||||
EnableBriefs bool `json:"enable_briefs"` // Morning/evening market briefs
|
EnableBriefs bool `json:"enable_briefs"`
|
||||||
EnableNews bool `json:"enable_news"` // News scanning
|
EnableNews bool `json:"enable_news"`
|
||||||
EnableSentinel bool `json:"enable_sentinel"` // Market anomaly detection
|
EnableSentinel bool `json:"enable_sentinel"`
|
||||||
BriefTimes []int `json:"brief_times"` // Hours to send briefs (e.g. [8, 20])
|
BriefTimes []int `json:"brief_times"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultConfig returns sensible defaults.
|
|
||||||
func DefaultConfig() *Config {
|
func DefaultConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Language: "zh",
|
Language: "zh", WatchSymbols: []string{"BTCUSDT", "ETHUSDT", "SOLUSDT"},
|
||||||
WatchSymbols: []string{"BTCUSDT", "ETHUSDT", "SOLUSDT"},
|
EnableBriefs: true, EnableNews: true, EnableSentinel: true, BriefTimes: []int{8, 20},
|
||||||
EnableBriefs: true,
|
|
||||||
EnableNews: true,
|
|
||||||
EnableSentinel: true,
|
|
||||||
BriefTimes: []int{8, 20},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new NOFXi Agent.
|
func New(tm *manager.TraderManager, st *store.Store, cfg *Config, logger *slog.Logger) *Agent {
|
||||||
func New(
|
if cfg == nil { cfg = DefaultConfig() }
|
||||||
traderMgr *manager.TraderManager,
|
return &Agent{traderManager: tm, store: st, config: cfg, logger: logger}
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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() {
|
func (a *Agent) EnsureAIClient() {
|
||||||
if a.aiClient != nil {
|
if a.aiClient != nil { return }
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to create from store AI model configs
|
|
||||||
if a.store != nil {
|
if a.store != nil {
|
||||||
models, err := a.store.AIModel().List("default")
|
models, err := a.store.AIModel().List("default")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -110,518 +63,299 @@ func (a *Agent) EnsureAIClient() {
|
|||||||
apiKey := string(m.APIKey)
|
apiKey := string(m.APIKey)
|
||||||
if apiKey != "" && m.CustomAPIURL != "" {
|
if apiKey != "" && m.CustomAPIURL != "" {
|
||||||
client := mcp.NewClient()
|
client := mcp.NewClient()
|
||||||
modelName := m.CustomModelName
|
name := m.CustomModelName
|
||||||
if modelName == "" {
|
if name == "" { name = m.ID }
|
||||||
modelName = m.ID
|
client.SetAPIKey(apiKey, m.CustomAPIURL, name)
|
||||||
}
|
|
||||||
client.SetAPIKey(apiKey, m.CustomAPIURL, modelName)
|
|
||||||
a.aiClient = client
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
a.logger.Warn("no AI client — agent will have limited capabilities")
|
||||||
a.logger.Warn("no AI client available — chat and analysis will be limited")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Start starts all agent services.
|
|
||||||
func (a *Agent) Start() {
|
func (a *Agent) Start() {
|
||||||
a.logger.Info("starting NOFXi agent...")
|
a.logger.Info("starting NOFXi agent...")
|
||||||
|
|
||||||
// Ensure we have an AI client
|
|
||||||
a.EnsureAIClient()
|
a.EnsureAIClient()
|
||||||
|
|
||||||
// Start sentinel (market anomaly detection)
|
|
||||||
if a.config.EnableSentinel {
|
if a.config.EnableSentinel {
|
||||||
a.sentinel = NewSentinel(a.config.WatchSymbols, a.handleSignal, a.logger)
|
a.sentinel = NewSentinel(a.config.WatchSymbols, a.handleSignal, a.logger)
|
||||||
a.sentinel.Start()
|
a.sentinel.Start()
|
||||||
a.logger.Info("sentinel started", "symbols", a.config.WatchSymbols)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start brain (proactive intelligence)
|
|
||||||
a.brain = NewBrain(a, a.logger)
|
a.brain = NewBrain(a, a.logger)
|
||||||
if a.config.EnableNews {
|
if a.config.EnableNews { a.brain.StartNewsScan(5 * time.Minute) }
|
||||||
a.brain.StartNewsScan(5 * time.Minute)
|
if a.config.EnableBriefs { a.brain.StartMarketBriefs(a.config.BriefTimes) }
|
||||||
a.logger.Info("news scanner started")
|
a.scheduler = NewScheduler(a, a.logger)
|
||||||
}
|
|
||||||
if a.config.EnableBriefs {
|
|
||||||
a.brain.StartMarketBriefs(a.config.BriefTimes)
|
|
||||||
a.logger.Info("market briefs enabled", "hours", a.config.BriefTimes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start scheduler
|
|
||||||
a.scheduler.Start(context.Background())
|
a.scheduler.Start(context.Background())
|
||||||
a.logger.Info("scheduler started")
|
|
||||||
|
|
||||||
a.logger.Info("NOFXi agent is online 🚀")
|
a.logger.Info("NOFXi agent is online 🚀")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop stops all agent services.
|
|
||||||
func (a *Agent) Stop() {
|
func (a *Agent) Stop() {
|
||||||
if a.sentinel != nil {
|
if a.sentinel != nil { a.sentinel.Stop() }
|
||||||
a.sentinel.Stop()
|
if a.brain != nil { a.brain.Stop() }
|
||||||
}
|
if a.scheduler != nil { a.scheduler.Stop() }
|
||||||
if a.brain != nil {
|
|
||||||
a.brain.Stop()
|
|
||||||
}
|
|
||||||
a.scheduler.Stop()
|
|
||||||
a.logger.Info("NOFXi agent stopped")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleMessage processes a user message and returns a response.
|
// HandleMessage — the core. Everything goes through the LLM.
|
||||||
// This is the main entry point for Telegram/Web interaction.
|
|
||||||
func (a *Agent) HandleMessage(ctx context.Context, userID int64, text string) (string, error) {
|
func (a *Agent) HandleMessage(ctx context.Context, userID int64, text string) (string, error) {
|
||||||
// Extract language from prefix [lang:xx]
|
|
||||||
lang := a.config.Language
|
lang := a.config.Language
|
||||||
if strings.HasPrefix(text, "[lang:") {
|
if strings.HasPrefix(text, "[lang:") {
|
||||||
if end := strings.Index(text, "] "); end > 0 {
|
if end := strings.Index(text, "] "); end > 0 {
|
||||||
lang = text[6:end]
|
lang = text[6:end]; text = text[end+2:]
|
||||||
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 {
|
if resp, handled := a.handleSetupFlow(userID, text, lang); handled {
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
intent := a.router.Route(text)
|
// Only handle bare slash commands directly (instant, no AI needed)
|
||||||
|
if text == "/help" || text == "/start" {
|
||||||
switch intent.Type {
|
|
||||||
case IntentHelp:
|
|
||||||
return a.msg(lang, "help"), nil
|
return a.msg(lang, "help"), nil
|
||||||
case IntentStatus:
|
}
|
||||||
|
if text == "/status" {
|
||||||
return a.handleStatus(lang), nil
|
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 ---
|
// 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) handleStatus(L string) string {
|
func (a *Agent) thinkAndAct(ctx context.Context, userID int64, lang, text string) (string, error) {
|
||||||
traderCount := 0
|
if a.aiClient == nil {
|
||||||
runningCount := 0
|
return a.noAIFallback(lang, text)
|
||||||
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++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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 {
|
if a.sentinel != nil {
|
||||||
watchCount = a.sentinel.SymbolCount()
|
watchlist = a.sentinel.FormatWatchlist(lang)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf(a.msg(L, "status"),
|
if lang == "zh" {
|
||||||
runningCount, traderCount, watchCount, time.Now().Format("2006-01-02 15:04:05"))
|
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) {
|
// gatherContext collects real-time market data relevant to the user's message.
|
||||||
raw := strings.ToLower(intent.Raw)
|
func (a *Agent) gatherContext(text string) string {
|
||||||
|
var parts []string
|
||||||
|
upper := strings.ToUpper(text)
|
||||||
|
|
||||||
// Get live data from NOFX trader manager
|
// Crypto — try to get real-time data
|
||||||
if a.traderManager == nil {
|
cryptoSymbols := []string{"BTC", "ETH", "SOL", "BNB", "XRP", "DOGE", "ADA", "AVAX", "DOT", "LINK"}
|
||||||
return a.msg(L, "no_traders"), nil
|
for _, sym := range cryptoSymbols {
|
||||||
}
|
if strings.Contains(upper, sym) {
|
||||||
|
md, err := market.Get(sym + "USDT")
|
||||||
// List all positions across all traders
|
if err == nil {
|
||||||
if strings.Contains(raw, "position") || strings.Contains(raw, "持仓") {
|
parts = append(parts, fmt.Sprintf("[%s/USDT Real-time]\nPrice: $%.4f | 1h: %+.2f%% | 4h: %+.2f%% | RSI7: %.1f | EMA20: %.4f | MACD: %.6f | Funding: %.4f%%",
|
||||||
return a.queryAllPositions(L)
|
sym, md.CurrentPrice, md.PriceChange1h, md.PriceChange4h, md.CurrentRSI7, md.CurrentEMA20, md.CurrentMACD, md.FundingRate*100))
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
hasPosition = true
|
}
|
||||||
pnl := toFloat(p["unrealizedPnl"])
|
}
|
||||||
e := "🟢"
|
|
||||||
if pnl < 0 {
|
// Trader positions
|
||||||
e = "🔴"
|
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 strings.Join(parts, "\n")
|
||||||
return a.msg(L, "no_positions"), nil
|
|
||||||
}
|
|
||||||
sb.WriteString(fmt.Sprintf(a.msg(L, "total_pnl"), totalPnL))
|
|
||||||
return sb.String(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
traders := a.traderManager.GetAllTraders()
|
||||||
if len(traders) == 0 {
|
if len(traders) == 0 { return "Traders: none configured" }
|
||||||
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"))
|
|
||||||
|
|
||||||
|
var lines []string
|
||||||
for id, t := range traders {
|
for id, t := range traders {
|
||||||
s := t.GetStatus()
|
s := t.GetStatus()
|
||||||
running, _ := s["is_running"].(bool)
|
running, _ := s["is_running"].(bool)
|
||||||
icon := "⏹"
|
status := "stopped"
|
||||||
if running {
|
if running { status = "running" }
|
||||||
icon = "▶️"
|
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
|
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 {
|
func toFloat(v interface{}) float64 {
|
||||||
switch x := v.(type) {
|
switch x := v.(type) {
|
||||||
case float64:
|
case float64: return x
|
||||||
return x
|
case float32: return float64(x)
|
||||||
case float32:
|
case int: return float64(x)
|
||||||
return float64(x)
|
case int64: return float64(x)
|
||||||
case int:
|
case string: f, _ := strconv.ParseFloat(x, 64); return f
|
||||||
return float64(x)
|
|
||||||
case int64:
|
|
||||||
return float64(x)
|
|
||||||
case string:
|
|
||||||
f, _ := strconv.ParseFloat(x, 64)
|
|
||||||
return f
|
|
||||||
}
|
}
|
||||||
return 0
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ var i18nMessages = map[string]map[string]string{
|
|||||||
"Just talk to me in any language 💬",
|
"Just talk to me in any language 💬",
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"zh": "📊 *NOFXi 状态*\n\n• 运行中 Traders: %d/%d\n• 监控交易对: %d\n• 时间: %s",
|
"zh": "📊 *NOFXi 状态*\n\n• Traders: %d/%d 运行中\n• 监控: %d 个交易对\n• AI: %s\n• 时间: %s",
|
||||||
"en": "📊 *NOFXi Status*\n\n• Running Traders: %d/%d\n• Watching: %d symbols\n• Time: %s",
|
"en": "📊 *NOFXi Status*\n\n• Traders: %d/%d running\n• Watching: %d symbols\n• AI: %s\n• Time: %s",
|
||||||
},
|
},
|
||||||
"no_traders": {
|
"no_traders": {
|
||||||
"zh": "📭 暂无 Trader。请在 Web UI 中创建和配置。",
|
"zh": "📭 暂无 Trader。请在 Web UI 中创建和配置。",
|
||||||
|
|||||||
108
agent/router.go
108
agent/router.go
@@ -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}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user