mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-15 16:56:56 +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:
192
nofxi/internal/execution/bridge.go
Normal file
192
nofxi/internal/execution/bridge.go
Normal 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)
|
||||
}
|
||||
47
nofxi/internal/execution/execution.go
Normal file
47
nofxi/internal/execution/execution.go
Normal 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"`
|
||||
}
|
||||
Reference in New Issue
Block a user