Files
nofx/nofxi/internal/thinking/llm.go
shinchan-zhai cf7bf16c28 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
2026-03-22 17:48:14 +08:00

145 lines
3.4 KiB
Go

package thinking
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// LLMEngine implements Engine using an OpenAI-compatible API.
// Works with OpenAI, claw402 (x402), DeepSeek, Qwen, etc.
type LLMEngine struct {
baseURL string
apiKey string
model string
httpClient *http.Client
}
// NewLLMEngine creates a new LLM-backed thinking engine.
func NewLLMEngine(baseURL, apiKey, model string) *LLMEngine {
if baseURL == "" {
baseURL = "https://api.openai.com/v1"
}
return &LLMEngine{
baseURL: baseURL,
apiKey: apiKey,
model: model,
httpClient: &http.Client{
Timeout: 120 * time.Second,
},
}
}
// chatRequest is the OpenAI chat completions request body.
type chatRequest struct {
Model string `json:"model"`
Messages []Message `json:"messages"`
}
// chatResponse is the OpenAI chat completions response body.
type chatResponse struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
Error *struct {
Message string `json:"message"`
} `json:"error,omitempty"`
}
// Chat sends messages to the LLM and returns the response.
func (e *LLMEngine) Chat(ctx context.Context, messages []Message) (string, error) {
reqBody := chatRequest{
Model: e.model,
Messages: messages,
}
body, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("marshal request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", e.baseURL+"/chat/completions", bytes.NewReader(body))
if err != nil {
return "", fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if e.apiKey != "" {
req.Header.Set("Authorization", "Bearer "+e.apiKey)
}
resp, err := e.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("http request: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("LLM API error (status %d): %s", resp.StatusCode, string(respBody))
}
var chatResp chatResponse
if err := json.Unmarshal(respBody, &chatResp); err != nil {
return "", fmt.Errorf("unmarshal response: %w", err)
}
if chatResp.Error != nil {
return "", fmt.Errorf("LLM error: %s", chatResp.Error.Message)
}
if len(chatResp.Choices) == 0 {
return "", fmt.Errorf("LLM returned no choices")
}
return chatResp.Choices[0].Message.Content, nil
}
// Analyze sends an analysis prompt and parses the AI response.
func (e *LLMEngine) Analyze(ctx context.Context, prompt string) (*Analysis, error) {
systemPrompt := `You are NOFXi, an expert AI trading analyst. Analyze the given market data and provide a trading recommendation.
Respond in JSON format:
{
"action": "buy|sell|hold|wait",
"symbol": "BTC/USDT",
"confidence": 0.85,
"reasoning": "Brief explanation",
"stop_loss": 0.0,
"take_profit": 0.0
}
Be concise. Only recommend high-confidence trades.`
messages := []Message{
{Role: "system", Content: systemPrompt},
{Role: "user", Content: prompt},
}
resp, err := e.Chat(ctx, messages)
if err != nil {
return nil, err
}
var analysis Analysis
if err := json.Unmarshal([]byte(resp), &analysis); err != nil {
// If JSON parsing fails, return the raw text as reasoning
return &Analysis{
Action: "hold",
Reasoning: resp,
}, nil
}
return &analysis, nil
}