Files
nofx/agent/brain.go
shinchan-zhai f095b6655b feat: NOFXi Agent integrated into NOFX core
This is the key architectural change: NOFXi is no longer a separate
sub-project in nofxi/, but a first-class module in agent/ that sits
on top of NOFX's existing engine.

agent/ package:
- agent.go: Core orchestrator, directly uses NOFX's TraderManager,
  Store, and Market packages. No wrapper/bridge needed.
- brain.go: Proactive intelligence (news scan, market briefs, signal handling)
- sentinel.go: Market anomaly detection (price breakout, volume spike, funding rate)
- scheduler.go: Daily reports, position risk checks
- router.go: Intent routing for natural language (中/英 bilingual)
- i18n.go: Full Chinese/English message catalog
- web.go: Agent REST API on :8900 (chat, klines, ticker)

main.go changes:
- Banner: NOFX → NOFXi
- Agent auto-starts with NOFX (sentinel + brain + scheduler + web)
- Zero breaking changes to existing NOFX functionality

Key difference from old nofxi/:
- Agent reads LIVE data from TraderManager (positions, balances, status)
- Uses market.Get() for full technical indicators (EMA/MACD/RSI/BB)
- No duplicate code — kernel, trader, store all used directly
- Old nofxi/ sub-project will be removed once migration is verified

Architecture:
  NOFX (engine) → kernel + trader + market + store + mcp + telegram
  NOFXi (brain) → agent/ package → uses all of the above
2026-03-25 01:05:54 +08:00

147 lines
4.0 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package agent
import (
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"strings"
"sync"
"time"
)
// Brain handles proactive intelligence: signals, news, market briefs.
type Brain struct {
agent *Agent
logger *slog.Logger
stopCh chan struct{}
recentSignals sync.Map // debounce
}
func NewBrain(agent *Agent, logger *slog.Logger) *Brain {
return &Brain{agent: agent, logger: logger, stopCh: make(chan struct{})}
}
func (b *Brain) Stop() { close(b.stopCh) }
func (b *Brain) HandleSignal(sig Signal) {
key := fmt.Sprintf("%s:%s", sig.Type, sig.Symbol)
if v, ok := b.recentSignals.Load(key); ok {
if time.Since(v.(time.Time)) < 10*time.Minute {
return
}
}
b.recentSignals.Store(key, time.Now())
emoji := map[string]string{"info": "", "warning": "⚠️", "critical": "🚨"}
e := emoji[sig.Severity]
if e == "" { e = "📊" }
b.agent.notifyAll(fmt.Sprintf("%s *%s*\n\n%s", e, sig.Title, sig.Detail))
}
func (b *Brain) StartNewsScan(interval time.Duration) {
seen := make(map[string]bool)
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-b.stopCh: return
case <-ticker.C:
b.scanNews(seen)
}
}
}()
}
func (b *Brain) scanNews(seen map[string]bool) {
resp, err := http.Get("https://min-api.cryptocompare.com/data/v2/news/?lang=EN&sortOrder=latest")
if err != nil { return }
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result struct {
Data []struct {
Title string `json:"title"`
Source string `json:"source"`
URL string `json:"url"`
Body string `json:"body"`
Categories string `json:"categories"`
PublishedOn int64 `json:"published_on"`
} `json:"Data"`
}
json.Unmarshal(body, &result)
bullish := []string{"surge", "rally", "bullish", "breakout", "ath", "pump", "adoption"}
bearish := []string{"crash", "dump", "bearish", "sell-off", "plunge", "hack", "ban", "fraud"}
for _, d := range result.Data {
if seen[d.URL] { continue }
seen[d.URL] = true
if time.Since(time.Unix(d.PublishedOn, 0)) > 10*time.Minute { continue }
lower := strings.ToLower(d.Title + " " + d.Body)
bc, brc := 0, 0
for _, w := range bullish { if strings.Contains(lower, w) { bc++ } }
for _, w := range bearish { if strings.Contains(lower, w) { brc++ } }
if bc == 0 && brc == 0 { continue }
emoji := "📰"
sentiment := "NEUTRAL"
if bc > brc { emoji = "🟢"; sentiment = "BULLISH" }
if brc > bc { emoji = "🔴"; sentiment = "BEARISH" }
b.agent.notifyAll(fmt.Sprintf("%s *News*\n\n%s\n\n• Source: %s\n• Sentiment: %s",
emoji, d.Title, d.Source, sentiment))
}
if len(seen) > 1000 { for k := range seen { delete(seen, k) } }
}
func (b *Brain) StartMarketBriefs(hours []int) {
go func() {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
sent := make(map[string]bool)
for {
select {
case <-b.stopCh: return
case now := <-ticker.C:
key := now.Format("2006-01-02-15")
for _, h := range hours {
if now.Hour() == h && now.Minute() == 30 && !sent[key] {
sent[key] = true
b.sendBrief(h)
}
}
}
}
}()
}
func (b *Brain) sendBrief(hour int) {
title := "☀️ *早间市场简报*"
if hour >= 18 { title = "🌙 *晚间市场简报*" }
// Fetch BTC/ETH prices for the brief
var btcPrice, ethPrice, btcChg, ethChg string
for _, sym := range []string{"BTCUSDT", "ETHUSDT"} {
resp, err := http.Get(fmt.Sprintf("https://fapi.binance.com/fapi/v1/ticker/24hr?symbol=%s", sym))
if err != nil { continue }
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
var t map[string]string
json.Unmarshal(body, &t)
if sym == "BTCUSDT" { btcPrice = t["lastPrice"]; btcChg = t["priceChangePercent"] }
if sym == "ETHUSDT" { ethPrice = t["lastPrice"]; ethChg = t["priceChangePercent"] }
}
brief := fmt.Sprintf("%s\n\n• BTC: $%s (%s%%)\n• ETH: $%s (%s%%)\n\n_%s_",
title, btcPrice, btcChg, ethPrice, ethChg, time.Now().Format("2006-01-02 15:04"))
b.agent.notifyAll(brief)
}