Files
nofx/nofxi/internal/agent/brain.go
shinchan-zhai 3e5b280987 feat(nofxi): proactive intelligence - Sentinel, Brain, Learner
🧠 Brain (proactive intelligence):
- Receives signals from Sentinel and decides when to notify
- Signal debouncing (same type+symbol within 10min)
- Morning brief at 08:30, evening brief at 20:30 (AI-generated)
- Crypto news scanning every 5 minutes
- Auto-filters high-impact news by sentiment + relevance

👁️ Sentinel (market anomaly detection):
- Watches BTC/ETH/SOL by default (expandable)
- Price breakout detection (>3% in 5 minutes)
- Volume spike detection (>3x average)
- Funding rate anomaly detection (>0.1%)
- 60-second scan interval, 1-hour price history buffer

📰 News Monitor:
- CryptoCompare API (free, no auth)
- Keyword-based sentiment classification (bullish/bearish/neutral)
- Symbol extraction from headlines
- Deduplication via seen URLs

📚 Learner (trading memory):
- User profile analysis (win rate, preferred side, risk tolerance)
- Trading lessons storage (win/loss patterns, strategy notes)
- AI prediction tracking (log predictions → resolve with actual P/L)
- Prediction accuracy metrics per model

NOFXi is no longer a passive chatbot. It watches, thinks, learns, and acts.
2026-03-22 23:02:46 +08:00

190 lines
4.3 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 (
"context"
"fmt"
"log/slog"
"strings"
"time"
"nofx/nofxi/internal/perception"
"nofx/nofxi/internal/thinking"
)
// Brain is the proactive intelligence layer.
// It receives signals from Sentinel, processes news, and decides
// when to proactively notify the user.
type Brain struct {
agent *Agent
news *perception.NewsMonitor
logger *slog.Logger
stopCh chan struct{}
// Debounce: don't spam the same signal
recentSignals map[string]time.Time
}
// NewBrain creates the proactive brain.
func NewBrain(agent *Agent, logger *slog.Logger) *Brain {
return &Brain{
agent: agent,
news: perception.NewNewsMonitor(logger),
logger: logger,
stopCh: make(chan struct{}),
recentSignals: make(map[string]time.Time),
}
}
// HandleSignal processes a market signal from the Sentinel.
func (b *Brain) HandleSignal(signal perception.Signal) {
// Debounce: same signal type + symbol within 10 minutes
key := fmt.Sprintf("%s:%s", signal.Type, signal.Symbol)
if last, ok := b.recentSignals[key]; ok && time.Since(last) < 10*time.Minute {
return
}
b.recentSignals[key] = time.Now()
// Format alert message
emoji := map[string]string{
"info": "",
"warning": "⚠️",
"critical": "🚨",
}
e := emoji[signal.Severity]
if e == "" {
e = "📊"
}
msg := fmt.Sprintf("%s *%s*\n\n%s\n\n_%s_",
e, signal.Title, signal.Detail, signal.Timestamp.Format("15:04:05"))
b.notifyAll(msg)
}
// StartNewsScan begins periodic news scanning.
func (b *Brain) StartNewsScan(interval time.Duration) {
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-b.stopCh:
return
case <-ticker.C:
b.scanNews()
}
}
}()
}
// StartMarketBrief sends a morning/evening market brief.
func (b *Brain) StartMarketBrief() {
go func() {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for {
select {
case <-b.stopCh:
return
case now := <-ticker.C:
hour := now.Hour()
minute := now.Minute()
// Morning brief at 08:30
if hour == 8 && minute == 30 {
b.sendMarketBrief("morning")
}
// Evening brief at 20:30
if hour == 20 && minute == 30 {
b.sendMarketBrief("evening")
}
}
}
}()
}
func (b *Brain) scanNews() {
items, err := b.news.FetchNews()
if err != nil {
b.logger.Error("fetch news", "error", err)
return
}
// Filter for high-impact news related to watched symbols
for _, item := range items {
if item.Sentiment == "neutral" {
continue
}
if len(item.Symbols) == 0 {
continue
}
// Only alert on recent news (last 10 minutes)
if time.Since(item.Timestamp) > 10*time.Minute {
continue
}
emoji := "📰"
if item.Sentiment == "bullish" {
emoji = "🟢"
} else if item.Sentiment == "bearish" {
emoji = "🔴"
}
msg := fmt.Sprintf("%s *%s*\n\n%s\n\n• Source: %s\n• Symbols: %s\n• Sentiment: %s",
emoji, "News Alert",
item.Title,
item.Source,
strings.Join(item.Symbols, ", "),
strings.ToUpper(item.Sentiment),
)
b.notifyAll(msg)
}
}
func (b *Brain) sendMarketBrief(timeOfDay string) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
prompt := fmt.Sprintf(`Generate a brief %s market summary for crypto trading.
Include: BTC/ETH price direction, key levels, market sentiment, any notable events.
Be concise (under 200 words). Use trading emojis. Respond in Chinese.
Current time: %s`, timeOfDay, time.Now().Format("2006-01-02 15:04:05"))
resp, err := b.agent.thinker.Chat(ctx, []thinking.Message{
{Role: "system", Content: "You are NOFXi, a professional crypto trading AI. Respond in Chinese."},
{Role: "user", Content: prompt},
})
if err != nil {
b.logger.Error("generate market brief", "error", err)
return
}
title := "☀️ *早间市场简报*"
if timeOfDay == "evening" {
title = "🌙 *晚间市场简报*"
}
msg := fmt.Sprintf("%s\n\n%s\n\n_Generated by NOFXi 🤖_", title, resp)
b.notifyAll(msg)
}
func (b *Brain) notifyAll(text string) {
if b.agent.NotifyFunc == nil {
return
}
for _, uid := range b.agent.config.Telegram.AllowedIDs {
if err := b.agent.NotifyFunc(uid, text); err != nil {
b.logger.Error("notify", "user_id", uid, "error", err)
}
}
}
// Stop stops the brain.
func (b *Brain) Stop() {
close(b.stopCh)
}