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:
shinchan-zhai
2026-03-22 17:48:14 +08:00
parent 4e4b4ceed7
commit cf7bf16c28
25 changed files with 2688 additions and 0 deletions

View File

@@ -0,0 +1,192 @@
package execution
import (
"fmt"
"log/slog"
"strings"
"sync"
)
// TraderFactory creates a NOFX Trader by exchange name.
// Injected from main.go where nofx exchange packages are available.
type TraderFactory func(exchange, apiKey, apiSecret, passphrase string, testnet bool) (NofxTrader, error)
// NofxTrader mirrors nofx/trader/types.Trader interface.
// We redefine it here to avoid a direct import cycle with the parent module.
type NofxTrader interface {
GetBalance() (map[string]interface{}, error)
GetPositions() ([]map[string]interface{}, error)
OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error)
OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error)
CloseLong(symbol string, quantity float64) (map[string]interface{}, error)
CloseShort(symbol string, quantity float64) (map[string]interface{}, error)
SetLeverage(symbol string, leverage int) error
GetMarketPrice(symbol string) (float64, error)
SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error
SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error
CancelAllOrders(symbol string) error
}
// Bridge connects NOFXi to NOFX trading engine.
type Bridge struct {
mu sync.RWMutex
traders map[string]NofxTrader // exchange name → trader
factory TraderFactory
logger *slog.Logger
}
// NewBridge creates a new execution bridge.
func NewBridge(factory TraderFactory, logger *slog.Logger) *Bridge {
return &Bridge{
traders: make(map[string]NofxTrader),
factory: factory,
logger: logger,
}
}
// RegisterTrader registers a pre-configured trader for an exchange.
func (b *Bridge) RegisterTrader(exchange string, trader NofxTrader) {
b.mu.Lock()
defer b.mu.Unlock()
b.traders[strings.ToLower(exchange)] = trader
b.logger.Info("trader registered", "exchange", exchange)
}
// getTrader returns the trader for the given exchange.
func (b *Bridge) getTrader(exchange string) (NofxTrader, error) {
b.mu.RLock()
defer b.mu.RUnlock()
t, ok := b.traders[strings.ToLower(exchange)]
if !ok {
return nil, fmt.Errorf("exchange %q not configured", exchange)
}
return t, nil
}
// PlaceOrder executes a trade via the NOFX trader.
func (b *Bridge) PlaceOrder(exchange, symbol, side string, quantity float64, leverage int) (map[string]interface{}, error) {
trader, err := b.getTrader(exchange)
if err != nil {
return nil, err
}
// Set leverage first
if leverage > 0 {
if err := trader.SetLeverage(symbol, leverage); err != nil {
b.logger.Warn("set leverage failed", "error", err)
}
}
side = strings.ToUpper(side)
switch side {
case "BUY", "LONG", "OPEN_LONG":
b.logger.Info("opening long", "exchange", exchange, "symbol", symbol, "qty", quantity, "leverage", leverage)
return trader.OpenLong(symbol, quantity, leverage)
case "SELL", "SHORT", "OPEN_SHORT":
b.logger.Info("opening short", "exchange", exchange, "symbol", symbol, "qty", quantity, "leverage", leverage)
return trader.OpenShort(symbol, quantity, leverage)
case "CLOSE_LONG":
b.logger.Info("closing long", "exchange", exchange, "symbol", symbol, "qty", quantity)
return trader.CloseLong(symbol, quantity)
case "CLOSE_SHORT":
b.logger.Info("closing short", "exchange", exchange, "symbol", symbol, "qty", quantity)
return trader.CloseShort(symbol, quantity)
default:
return nil, fmt.Errorf("unknown side: %s", side)
}
}
// GetPositions returns all open positions from an exchange.
func (b *Bridge) GetPositions(exchange string) ([]Position, error) {
trader, err := b.getTrader(exchange)
if err != nil {
return nil, err
}
raw, err := trader.GetPositions()
if err != nil {
return nil, fmt.Errorf("get positions: %w", err)
}
var positions []Position
for _, p := range raw {
pos := Position{
Exchange: exchange,
Symbol: fmt.Sprint(p["symbol"]),
Side: fmt.Sprint(p["side"]),
}
if v, ok := p["size"].(float64); ok {
pos.Size = v
}
if v, ok := p["entryPrice"].(float64); ok {
pos.EntryPrice = v
}
if v, ok := p["markPrice"].(float64); ok {
pos.MarkPrice = v
}
if v, ok := p["unrealizedPnl"].(float64); ok {
pos.PnL = v
}
if v, ok := p["leverage"].(float64); ok {
pos.Leverage = v
}
// Skip empty positions
if pos.Size != 0 {
positions = append(positions, pos)
}
}
return positions, nil
}
// GetBalance returns account balance from an exchange.
func (b *Bridge) GetBalance(exchange string) (*Balance, error) {
trader, err := b.getTrader(exchange)
if err != nil {
return nil, err
}
raw, err := trader.GetBalance()
if err != nil {
return nil, fmt.Errorf("get balance: %w", err)
}
bal := &Balance{
Exchange: exchange,
Currency: "USDT",
}
if v, ok := raw["totalBalance"].(float64); ok {
bal.Total = v
}
if v, ok := raw["availableBalance"].(float64); ok {
bal.Available = v
}
bal.InPosition = bal.Total - bal.Available
return bal, nil
}
// GetPrice returns the current market price for a symbol.
func (b *Bridge) GetPrice(exchange, symbol string) (float64, error) {
trader, err := b.getTrader(exchange)
if err != nil {
return 0, err
}
return trader.GetMarketPrice(symbol)
}
// SetStopLoss sets a stop-loss order.
func (b *Bridge) SetStopLoss(exchange, symbol, positionSide string, quantity, price float64) error {
trader, err := b.getTrader(exchange)
if err != nil {
return err
}
return trader.SetStopLoss(symbol, positionSide, quantity, price)
}
// SetTakeProfit sets a take-profit order.
func (b *Bridge) SetTakeProfit(exchange, symbol, positionSide string, quantity, price float64) error {
trader, err := b.getTrader(exchange)
if err != nil {
return err
}
return trader.SetTakeProfit(symbol, positionSide, quantity, price)
}

View File

@@ -0,0 +1,47 @@
// Package execution implements the Execution Layer.
//
// Phase 2 — stubs for now.
//
// Planned capabilities:
// - Multi-exchange order execution (via NOFX engine)
// - Position management (open/close/adjust)
// - Stop-loss and take-profit management
// - x402 payment integration via Claw402
// - Safe mode and risk controls
package execution
// Executor is the trade execution interface.
type Executor interface {
// PlaceOrder places a trade order on the exchange.
PlaceOrder(exchange, symbol, side string, price, quantity float64) (string, error)
// ClosePosition closes an existing position.
ClosePosition(exchange, symbol string) error
// GetPositions returns current open positions.
GetPositions(exchange string) ([]Position, error)
// GetBalance returns account balance.
GetBalance(exchange string) (*Balance, error)
}
// Position represents an open position.
type Position struct {
Exchange string `json:"exchange"`
Symbol string `json:"symbol"`
Side string `json:"side"`
Size float64 `json:"size"`
EntryPrice float64 `json:"entry_price"`
MarkPrice float64 `json:"mark_price"`
PnL float64 `json:"pnl"`
Leverage float64 `json:"leverage"`
}
// Balance represents account balance.
type Balance struct {
Exchange string `json:"exchange"`
Total float64 `json:"total"`
Available float64 `json:"available"`
InPosition float64 `json:"in_position"`
Currency string `json:"currency"`
}