mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-17 17:34:39 +08:00
feat: init nofxi - AI Trading Agent module
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
This commit is contained in:
216
nofxi/internal/memory/store.go
Normal file
216
nofxi/internal/memory/store.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// Store is the SQLite-backed memory store.
|
||||
type Store struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NewStore opens (or creates) the SQLite database.
|
||||
func NewStore(dbPath string) (*Store, error) {
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open db: %w", err)
|
||||
}
|
||||
|
||||
// Enable WAL mode for better concurrency
|
||||
if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil {
|
||||
return nil, fmt.Errorf("set WAL: %w", err)
|
||||
}
|
||||
|
||||
s := &Store{db: db}
|
||||
if err := s.migrate(); err != nil {
|
||||
return nil, fmt.Errorf("migrate: %w", err)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Close closes the database.
|
||||
func (s *Store) Close() error {
|
||||
return s.db.Close()
|
||||
}
|
||||
|
||||
func (s *Store) migrate() error {
|
||||
queries := []string{
|
||||
`CREATE TABLE IF NOT EXISTS trades (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
exchange TEXT NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
side TEXT NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 'market',
|
||||
price REAL NOT NULL,
|
||||
quantity REAL NOT NULL,
|
||||
pnl REAL DEFAULT 0,
|
||||
fee REAL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'open',
|
||||
ai_model TEXT,
|
||||
ai_reason TEXT,
|
||||
strategy_id TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
closed_at DATETIME
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS conversations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS user_preferences (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_id, key)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS strategy_performance (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
strategy_id TEXT NOT NULL UNIQUE,
|
||||
strategy_name TEXT,
|
||||
total_trades INTEGER DEFAULT 0,
|
||||
win_rate REAL DEFAULT 0,
|
||||
total_pnl REAL DEFAULT 0,
|
||||
max_drawdown REAL DEFAULT 0,
|
||||
sharpe_ratio REAL DEFAULT 0,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS market_snapshots (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
symbol TEXT NOT NULL,
|
||||
price REAL NOT NULL,
|
||||
volume_24h REAL,
|
||||
change_24h REAL,
|
||||
note TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_trades_symbol ON trades(symbol)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_trades_status ON trades(status)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_conversations_user ON conversations(user_id, created_at)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_snapshots_symbol ON market_snapshots(symbol, created_at)`,
|
||||
}
|
||||
|
||||
for _, q := range queries {
|
||||
if _, err := s.db.Exec(q); err != nil {
|
||||
return fmt.Errorf("exec %q: %w", q[:40], err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Trade Operations ---
|
||||
|
||||
// SaveTrade inserts a new trade record.
|
||||
func (s *Store) SaveTrade(t *TradeRecord) (int64, error) {
|
||||
res, err := s.db.Exec(
|
||||
`INSERT INTO trades (exchange, symbol, side, type, price, quantity, pnl, fee, status, ai_model, ai_reason, strategy_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
t.Exchange, t.Symbol, t.Side, t.Type, t.Price, t.Quantity,
|
||||
t.PnL, t.Fee, t.Status, t.AIModel, t.AIReason, t.StrategyID,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.LastInsertId()
|
||||
}
|
||||
|
||||
// GetRecentTrades returns the last N trades.
|
||||
func (s *Store) GetRecentTrades(limit int) ([]TradeRecord, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT id, exchange, symbol, side, type, price, quantity, pnl, fee, status,
|
||||
COALESCE(ai_model,''), COALESCE(ai_reason,''), COALESCE(strategy_id,''), created_at, closed_at
|
||||
FROM trades ORDER BY created_at DESC LIMIT ?`, limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var trades []TradeRecord
|
||||
for rows.Next() {
|
||||
var t TradeRecord
|
||||
var closedAt sql.NullTime
|
||||
if err := rows.Scan(&t.ID, &t.Exchange, &t.Symbol, &t.Side, &t.Type,
|
||||
&t.Price, &t.Quantity, &t.PnL, &t.Fee, &t.Status,
|
||||
&t.AIModel, &t.AIReason, &t.StrategyID, &t.CreatedAt, &closedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if closedAt.Valid {
|
||||
t.ClosedAt = &closedAt.Time
|
||||
}
|
||||
trades = append(trades, t)
|
||||
}
|
||||
return trades, nil
|
||||
}
|
||||
|
||||
// --- Conversation Operations ---
|
||||
|
||||
// SaveMessage stores a conversation message.
|
||||
func (s *Store) SaveMessage(userID int64, role, content string) error {
|
||||
_, err := s.db.Exec(
|
||||
`INSERT INTO conversations (user_id, role, content) VALUES (?, ?, ?)`,
|
||||
userID, role, content,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetRecentMessages returns the last N messages for a user.
|
||||
func (s *Store) GetRecentMessages(userID int64, limit int) ([]Conversation, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT id, user_id, role, content, created_at FROM conversations
|
||||
WHERE user_id = ? ORDER BY created_at DESC LIMIT ?`, userID, limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var msgs []Conversation
|
||||
for rows.Next() {
|
||||
var c Conversation
|
||||
if err := rows.Scan(&c.ID, &c.UserID, &c.Role, &c.Content, &c.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msgs = append(msgs, c)
|
||||
}
|
||||
|
||||
// Reverse to get chronological order
|
||||
for i, j := 0, len(msgs)-1; i < j; i, j = i+1, j-1 {
|
||||
msgs[i], msgs[j] = msgs[j], msgs[i]
|
||||
}
|
||||
return msgs, nil
|
||||
}
|
||||
|
||||
// --- Preference Operations ---
|
||||
|
||||
// SetPreference upserts a user preference.
|
||||
func (s *Store) SetPreference(userID int64, key, value string) error {
|
||||
_, err := s.db.Exec(
|
||||
`INSERT INTO user_preferences (user_id, key, value, updated_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(user_id, key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
|
||||
userID, key, value, time.Now(),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetPreference retrieves a user preference.
|
||||
func (s *Store) GetPreference(userID int64, key string) (string, error) {
|
||||
var value string
|
||||
err := s.db.QueryRow(
|
||||
`SELECT value FROM user_preferences WHERE user_id = ? AND key = ?`,
|
||||
userID, key,
|
||||
).Scan(&value)
|
||||
if err == sql.ErrNoRows {
|
||||
return "", nil
|
||||
}
|
||||
return value, err
|
||||
}
|
||||
Reference in New Issue
Block a user