mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-15 08:46:58 +08:00
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.
This commit is contained in:
189
nofxi/internal/agent/brain.go
Normal file
189
nofxi/internal/agent/brain.go
Normal file
@@ -0,0 +1,189 @@
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user