mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-10 06:20:58 +08:00
NOFXi — Your AI Trading Agent, built on NOFX. Architecture: - Agent Core: intent routing, conversation memory, trade confirmation - Memory: SQLite (trades, conversations, preferences, strategies) - Thinking: LLM client (OpenAI/claw402/DeepSeek compatible) - Execution: Bridge to NOFX trader engine (9 exchanges) - Perception: Market monitor, price alerts, anomaly detection - Interaction: Telegram bot + REST API + OpenAI-compatible endpoint Features: - Natural language trading (中英文) - /buy /sell /analyze /watch /alert /positions /balance - Daily P/L reports, portfolio risk checks - Trade confirmation flow (safety first) - Price polling and alert notifications
215 lines
4.6 KiB
Go
215 lines
4.6 KiB
Go
package perception
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// PriceAlert triggers when a symbol hits a threshold.
|
|
type PriceAlert struct {
|
|
Symbol string
|
|
Price float64
|
|
Direction string // "above" or "below"
|
|
Threshold float64
|
|
Triggered time.Time
|
|
}
|
|
|
|
// AlertCallback is called when a price alert triggers.
|
|
type AlertCallback func(alert PriceAlert)
|
|
|
|
// MarketMonitor watches symbols and detects price anomalies.
|
|
type MarketMonitor struct {
|
|
mu sync.RWMutex
|
|
watching map[string]*watchState
|
|
alerts []alertRule
|
|
onAlert AlertCallback
|
|
httpClient *http.Client
|
|
logger *slog.Logger
|
|
stopCh chan struct{}
|
|
pollInterval time.Duration
|
|
}
|
|
|
|
type watchState struct {
|
|
Symbol string
|
|
LastPrice float64
|
|
Change1h float64
|
|
Change24h float64
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
type alertRule struct {
|
|
Symbol string
|
|
Direction string // "above" or "below"
|
|
Threshold float64
|
|
Fired bool
|
|
}
|
|
|
|
// NewMarketMonitor creates a new market monitor.
|
|
func NewMarketMonitor(onAlert AlertCallback, logger *slog.Logger) *MarketMonitor {
|
|
return &MarketMonitor{
|
|
watching: make(map[string]*watchState),
|
|
onAlert: onAlert,
|
|
httpClient: &http.Client{Timeout: 10 * time.Second},
|
|
logger: logger,
|
|
stopCh: make(chan struct{}),
|
|
pollInterval: 30 * time.Second,
|
|
}
|
|
}
|
|
|
|
// Watch starts monitoring a symbol.
|
|
func (m *MarketMonitor) Watch(symbol string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.watching[symbol] = &watchState{Symbol: symbol}
|
|
m.logger.Info("watching symbol", "symbol", symbol)
|
|
}
|
|
|
|
// Unwatch stops monitoring a symbol.
|
|
func (m *MarketMonitor) Unwatch(symbol string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
delete(m.watching, symbol)
|
|
}
|
|
|
|
// AddAlert adds a price alert rule.
|
|
func (m *MarketMonitor) AddAlert(symbol, direction string, threshold float64) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.alerts = append(m.alerts, alertRule{
|
|
Symbol: symbol,
|
|
Direction: direction,
|
|
Threshold: threshold,
|
|
})
|
|
m.logger.Info("alert added", "symbol", symbol, "direction", direction, "threshold", threshold)
|
|
}
|
|
|
|
// GetSnapshot returns the latest state for a symbol.
|
|
func (m *MarketMonitor) GetSnapshot(symbol string) (*watchState, bool) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
s, ok := m.watching[symbol]
|
|
return s, ok
|
|
}
|
|
|
|
// GetAllSnapshots returns all watched symbols.
|
|
func (m *MarketMonitor) GetAllSnapshots() map[string]*watchState {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
result := make(map[string]*watchState)
|
|
for k, v := range m.watching {
|
|
result[k] = v
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Start begins the monitoring loop.
|
|
func (m *MarketMonitor) Start() {
|
|
go m.loop()
|
|
}
|
|
|
|
// Stop stops the monitoring loop.
|
|
func (m *MarketMonitor) Stop() {
|
|
close(m.stopCh)
|
|
}
|
|
|
|
func (m *MarketMonitor) loop() {
|
|
ticker := time.NewTicker(m.pollInterval)
|
|
defer ticker.Stop()
|
|
|
|
// Initial fetch
|
|
m.updatePrices()
|
|
|
|
for {
|
|
select {
|
|
case <-m.stopCh:
|
|
return
|
|
case <-ticker.C:
|
|
m.updatePrices()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *MarketMonitor) updatePrices() {
|
|
m.mu.RLock()
|
|
symbols := make([]string, 0, len(m.watching))
|
|
for s := range m.watching {
|
|
symbols = append(symbols, s)
|
|
}
|
|
m.mu.RUnlock()
|
|
|
|
for _, symbol := range symbols {
|
|
price, err := m.fetchPrice(symbol)
|
|
if err != nil {
|
|
m.logger.Error("fetch price", "symbol", symbol, "error", err)
|
|
continue
|
|
}
|
|
|
|
m.mu.Lock()
|
|
if state, ok := m.watching[symbol]; ok {
|
|
state.LastPrice = price
|
|
state.UpdatedAt = time.Now()
|
|
}
|
|
|
|
// Check alerts
|
|
for i := range m.alerts {
|
|
a := &m.alerts[i]
|
|
if a.Symbol != symbol || a.Fired {
|
|
continue
|
|
}
|
|
triggered := false
|
|
if a.Direction == "above" && price >= a.Threshold {
|
|
triggered = true
|
|
} else if a.Direction == "below" && price <= a.Threshold {
|
|
triggered = true
|
|
}
|
|
if triggered {
|
|
a.Fired = true
|
|
if m.onAlert != nil {
|
|
m.onAlert(PriceAlert{
|
|
Symbol: symbol,
|
|
Price: price,
|
|
Direction: a.Direction,
|
|
Threshold: a.Threshold,
|
|
Triggered: time.Now(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
m.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
// fetchPrice gets current price from Binance public API (no auth needed).
|
|
func (m *MarketMonitor) fetchPrice(symbol string) (float64, error) {
|
|
url := fmt.Sprintf("https://fapi.binance.com/fapi/v1/ticker/price?symbol=%s", symbol)
|
|
resp, err := m.httpClient.Get(url)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
var result struct {
|
|
Price string `json:"price"`
|
|
}
|
|
if err := json.Unmarshal(body, &result); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
var price float64
|
|
fmt.Sscanf(result.Price, "%f", &price)
|
|
if price == 0 {
|
|
return 0, fmt.Errorf("invalid price for %s", symbol)
|
|
}
|
|
return price, nil
|
|
}
|