mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-07 21:12:00 +08:00
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
100 lines
2.3 KiB
Go
100 lines
2.3 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Scheduler struct {
|
|
agent *Agent
|
|
logger *slog.Logger
|
|
stopCh chan struct{}
|
|
}
|
|
|
|
func NewScheduler(a *Agent, l *slog.Logger) *Scheduler {
|
|
return &Scheduler{agent: a, logger: l, stopCh: make(chan struct{})}
|
|
}
|
|
|
|
func (s *Scheduler) Start(ctx context.Context) {
|
|
go func() {
|
|
ticker := time.NewTicker(1 * time.Minute)
|
|
defer ticker.Stop()
|
|
lastReport := time.Time{}
|
|
lastCheck := time.Time{}
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done(): return
|
|
case <-s.stopCh: return
|
|
case now := <-ticker.C:
|
|
// Daily report at 21:00
|
|
if now.Hour() == 21 && now.Sub(lastReport) > 12*time.Hour {
|
|
s.dailyReport()
|
|
lastReport = now
|
|
}
|
|
// Position risk check every 4h
|
|
if now.Sub(lastCheck) > 4*time.Hour {
|
|
s.riskCheck()
|
|
lastCheck = now
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (s *Scheduler) Stop() { close(s.stopCh) }
|
|
|
|
func (s *Scheduler) dailyReport() {
|
|
if s.agent.traderManager == nil { return }
|
|
|
|
traders := s.agent.traderManager.GetAllTraders()
|
|
if len(traders) == 0 { return }
|
|
|
|
var sb strings.Builder
|
|
sb.WriteString(fmt.Sprintf("📊 *NOFXi 每日报告 — %s*\n\n", time.Now().Format("2006-01-02")))
|
|
|
|
totalPnL := 0.0
|
|
for _, t := range traders {
|
|
info, err := t.GetAccountInfo()
|
|
if err != nil { continue }
|
|
equity := toFloat(info["total_equity"])
|
|
pnl := toFloat(info["unrealized_pnl"])
|
|
sb.WriteString(fmt.Sprintf("• %s: $%.2f (P/L: $%.2f)\n", t.GetName(), equity, pnl))
|
|
totalPnL += pnl
|
|
}
|
|
e := "📈"
|
|
if totalPnL < 0 { e = "📉" }
|
|
sb.WriteString(fmt.Sprintf("\n%s Total P/L: $%.2f", e, totalPnL))
|
|
|
|
s.agent.notifyAll(sb.String())
|
|
}
|
|
|
|
func (s *Scheduler) riskCheck() {
|
|
if s.agent.traderManager == nil { return }
|
|
|
|
var alerts []string
|
|
for _, t := range s.agent.traderManager.GetAllTraders() {
|
|
positions, err := t.GetPositions()
|
|
if err != nil { continue }
|
|
for _, p := range positions {
|
|
pnl := toFloat(p["unrealizedPnl"])
|
|
size := toFloat(p["size"])
|
|
if size == 0 { continue }
|
|
entry := toFloat(p["entryPrice"])
|
|
if entry > 0 {
|
|
pnlPct := (pnl / (entry * size)) * 100
|
|
if pnlPct < -5 {
|
|
alerts = append(alerts, fmt.Sprintf("⚠️ *%s* %s: %.1f%% ($%.2f)",
|
|
p["symbol"], p["side"], pnlPct, pnl))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if len(alerts) > 0 {
|
|
s.agent.notifyAll("🚨 *持仓风险提醒*\n\n" + strings.Join(alerts, "\n"))
|
|
}
|
|
}
|