mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-19 02:14:35 +08:00
Compare commits
3 Commits
5ea9a3990e
...
moltbot-no
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b4ce279da | ||
|
|
f9d8318869 | ||
|
|
01ba348841 |
47
.env.example
47
.env.example
@@ -49,12 +49,51 @@ RSA_PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY-----\nYOUR_KEY_HERE\n-----END RSA PRI
|
||||
TRANSPORT_ENCRYPTION=false
|
||||
|
||||
# ===========================================
|
||||
# Optional: External Services
|
||||
# Telegram AI Assistant (NEW - moltbot-nofx)
|
||||
# ===========================================
|
||||
|
||||
# Telegram notifications (optional)
|
||||
# TELEGRAM_BOT_TOKEN=your-bot-token
|
||||
# TELEGRAM_CHAT_ID=your-chat-id
|
||||
# Telegram Bot Token (get from @BotFather)
|
||||
# This enables the AI trading assistant via Telegram
|
||||
TELEGRAM_BOT_TOKEN=
|
||||
|
||||
# Allowed users (comma-separated Telegram user IDs)
|
||||
# Leave empty to allow all users (not recommended for production)
|
||||
# Get your ID from @userinfobot
|
||||
TELEGRAM_ALLOWED_USERS=
|
||||
|
||||
# Admin users (comma-separated Telegram user IDs)
|
||||
# Admins can manage bot settings
|
||||
TELEGRAM_ADMIN_USERS=
|
||||
|
||||
# Rate limit (messages per minute per user)
|
||||
TELEGRAM_RATE_LIMIT=30
|
||||
|
||||
# Default language: "en" or "zh"
|
||||
TELEGRAM_LANGUAGE=zh
|
||||
|
||||
# ===========================================
|
||||
# AI Model Configuration (for Assistant)
|
||||
# ===========================================
|
||||
|
||||
# DeepSeek (recommended - cost-effective)
|
||||
DEEPSEEK_API_KEY=
|
||||
DEEPSEEK_API_URL=
|
||||
DEEPSEEK_MODEL=deepseek-chat
|
||||
|
||||
# Claude (optional alternative)
|
||||
CLAUDE_API_KEY=
|
||||
CLAUDE_API_URL=
|
||||
CLAUDE_MODEL=
|
||||
|
||||
# OpenAI (optional alternative)
|
||||
OPENAI_API_KEY=
|
||||
OPENAI_API_URL=
|
||||
OPENAI_MODEL=
|
||||
|
||||
# Qwen (optional alternative)
|
||||
QWEN_API_KEY=
|
||||
QWEN_API_URL=
|
||||
QWEN_MODEL=
|
||||
|
||||
DB_TYPE=postgres
|
||||
DB_HOST=10.
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -124,3 +124,4 @@ dmypy.json
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
PR_DESCRIPTION.md
|
||||
nofx-moltbot
|
||||
|
||||
26
README.md
26
README.md
@@ -1,21 +1,9 @@
|
||||
<h1 align="center">NOFX — Open Source AI Trading OS</h1>
|
||||
# NOFX - Agentic Trading OS
|
||||
|
||||
<p align="center">
|
||||
<strong>The infrastructure layer for AI-powered financial trading.</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/NoFxAiOS/nofx/stargazers"><img src="https://img.shields.io/github/stars/NoFxAiOS/nofx?style=for-the-badge" alt="Stars"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/releases"><img src="https://img.shields.io/github/v/release/NoFxAiOS/nofx?style=for-the-badge" alt="Release"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-AGPL--3.0-blue.svg?style=for-the-badge" alt="License"></a>
|
||||
<a href="https://t.me/nofx_dev_community"><img src="https://img.shields.io/badge/Telegram-Community-blue?style=for-the-badge&logo=telegram" alt="Telegram"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://golang.org/"><img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go" alt="Go"></a>
|
||||
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
||||
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript" alt="TypeScript"></a>
|
||||
</p>
|
||||
[](https://golang.org/)
|
||||
[](https://reactjs.org/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](LICENSE)
|
||||
|
||||
| CONTRIBUTOR AIRDROP PROGRAM |
|
||||
|:----------------------------------:|
|
||||
@@ -26,6 +14,10 @@
|
||||
|
||||
---
|
||||
|
||||
## AI-Powered Multi-Asset Trading Platform
|
||||
|
||||
**NOFX** is an open-source AI trading system that lets you run multiple AI models to trade automatically. Configure strategies through a web interface, monitor performance in real-time, and let AI agents compete to find the best trading approach.
|
||||
|
||||
### Supported Markets
|
||||
|
||||
| Market | Trading | Status |
|
||||
|
||||
350
assistant/agent.go
Normal file
350
assistant/agent.go
Normal file
@@ -0,0 +1,350 @@
|
||||
// Package assistant implements the AI Agent runtime with tool calling capabilities
|
||||
// Inspired by moltbot's agent architecture, specialized for trading
|
||||
package assistant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"nofx/logger"
|
||||
"nofx/mcp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Agent represents an AI assistant with tool-calling capabilities
|
||||
type Agent struct {
|
||||
// AI client for LLM calls
|
||||
aiClient mcp.AIClient
|
||||
|
||||
// Tool registry
|
||||
tools map[string]Tool
|
||||
toolsLock sync.RWMutex
|
||||
|
||||
// Session/memory management
|
||||
sessions map[string]*Session
|
||||
sessionsLock sync.RWMutex
|
||||
|
||||
// Configuration
|
||||
config AgentConfig
|
||||
|
||||
// System prompt
|
||||
systemPrompt string
|
||||
}
|
||||
|
||||
// AgentConfig holds agent configuration
|
||||
type AgentConfig struct {
|
||||
// Max tool calls per turn (prevent infinite loops)
|
||||
MaxToolCalls int `json:"max_tool_calls"`
|
||||
|
||||
// Max conversation history to keep
|
||||
MaxHistoryMessages int `json:"max_history_messages"`
|
||||
|
||||
// Timeout for single AI call
|
||||
AITimeout time.Duration `json:"ai_timeout"`
|
||||
|
||||
// Model to use
|
||||
Model string `json:"model"`
|
||||
}
|
||||
|
||||
// DefaultAgentConfig returns sensible defaults
|
||||
func DefaultAgentConfig() AgentConfig {
|
||||
return AgentConfig{
|
||||
MaxToolCalls: 10,
|
||||
MaxHistoryMessages: 50,
|
||||
AITimeout: 120 * time.Second,
|
||||
Model: "deepseek-chat",
|
||||
}
|
||||
}
|
||||
|
||||
// NewAgent creates a new AI agent
|
||||
func NewAgent(aiClient mcp.AIClient, config AgentConfig) *Agent {
|
||||
agent := &Agent{
|
||||
aiClient: aiClient,
|
||||
tools: make(map[string]Tool),
|
||||
sessions: make(map[string]*Session),
|
||||
config: config,
|
||||
}
|
||||
|
||||
// Set default system prompt
|
||||
agent.systemPrompt = DefaultTradingSystemPrompt()
|
||||
|
||||
return agent
|
||||
}
|
||||
|
||||
// RegisterTool adds a tool to the agent's toolkit
|
||||
func (a *Agent) RegisterTool(tool Tool) {
|
||||
a.toolsLock.Lock()
|
||||
defer a.toolsLock.Unlock()
|
||||
a.tools[tool.Name()] = tool
|
||||
logger.Infof("🔧 Registered tool: %s", tool.Name())
|
||||
}
|
||||
|
||||
// RegisterTools adds multiple tools
|
||||
func (a *Agent) RegisterTools(tools ...Tool) {
|
||||
for _, tool := range tools {
|
||||
a.RegisterTool(tool)
|
||||
}
|
||||
}
|
||||
|
||||
// SetSystemPrompt sets the agent's system prompt
|
||||
func (a *Agent) SetSystemPrompt(prompt string) {
|
||||
a.systemPrompt = prompt
|
||||
}
|
||||
|
||||
// GetSession returns or creates a session for the given ID
|
||||
func (a *Agent) GetSession(sessionID string) *Session {
|
||||
a.sessionsLock.Lock()
|
||||
defer a.sessionsLock.Unlock()
|
||||
|
||||
if session, ok := a.sessions[sessionID]; ok {
|
||||
return session
|
||||
}
|
||||
|
||||
session := NewSession(sessionID, a.config.MaxHistoryMessages)
|
||||
a.sessions[sessionID] = session
|
||||
return session
|
||||
}
|
||||
|
||||
// Chat processes a user message and returns the agent's response
|
||||
// This is the main entry point for the agent loop
|
||||
func (a *Agent) Chat(ctx context.Context, sessionID string, userMessage string) (*AgentResponse, error) {
|
||||
session := a.GetSession(sessionID)
|
||||
|
||||
// Add user message to history
|
||||
session.AddMessage(Message{
|
||||
Role: "user",
|
||||
Content: userMessage,
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
|
||||
// Build the full prompt with tools
|
||||
systemPrompt := a.buildSystemPromptWithTools()
|
||||
conversationPrompt := a.buildConversationPrompt(session)
|
||||
|
||||
// Agent loop - keep calling AI until it's done or max iterations
|
||||
var finalResponse string
|
||||
toolCallCount := 0
|
||||
|
||||
for {
|
||||
// Check context cancellation
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
// Check max tool calls
|
||||
if toolCallCount >= a.config.MaxToolCalls {
|
||||
logger.Warnf("⚠️ Max tool calls reached (%d), stopping agent loop", a.config.MaxToolCalls)
|
||||
break
|
||||
}
|
||||
|
||||
// Call AI
|
||||
response, err := a.aiClient.CallWithMessages(systemPrompt, conversationPrompt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("AI call failed: %w", err)
|
||||
}
|
||||
|
||||
// Parse response for tool calls
|
||||
toolCalls, textResponse, err := a.parseResponse(response)
|
||||
if err != nil {
|
||||
// If parsing fails, treat entire response as text
|
||||
finalResponse = response
|
||||
break
|
||||
}
|
||||
|
||||
// If no tool calls, we're done
|
||||
if len(toolCalls) == 0 {
|
||||
finalResponse = textResponse
|
||||
break
|
||||
}
|
||||
|
||||
// Execute tool calls
|
||||
toolResults := a.executeToolCalls(ctx, toolCalls)
|
||||
toolCallCount += len(toolCalls)
|
||||
|
||||
// Add tool calls and results to conversation for next iteration
|
||||
conversationPrompt += fmt.Sprintf("\n\nAssistant called tools:\n%s\n\nTool results:\n%s\n\nBased on the tool results, please provide your response to the user:",
|
||||
formatToolCalls(toolCalls),
|
||||
formatToolResults(toolResults))
|
||||
|
||||
// If there's also a text response, capture it
|
||||
if textResponse != "" {
|
||||
finalResponse = textResponse
|
||||
}
|
||||
}
|
||||
|
||||
// Add assistant response to history
|
||||
session.AddMessage(Message{
|
||||
Role: "assistant",
|
||||
Content: finalResponse,
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
|
||||
return &AgentResponse{
|
||||
Text: finalResponse,
|
||||
SessionID: sessionID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// buildSystemPromptWithTools creates the system prompt including tool definitions
|
||||
func (a *Agent) buildSystemPromptWithTools() string {
|
||||
a.toolsLock.RLock()
|
||||
defer a.toolsLock.RUnlock()
|
||||
|
||||
var toolDefs []string
|
||||
for _, tool := range a.tools {
|
||||
toolDef := fmt.Sprintf(`- **%s**: %s
|
||||
Parameters: %s`, tool.Name(), tool.Description(), tool.ParameterSchema())
|
||||
toolDefs = append(toolDefs, toolDef)
|
||||
}
|
||||
|
||||
toolsSection := ""
|
||||
if len(toolDefs) > 0 {
|
||||
toolsSection = fmt.Sprintf(`
|
||||
|
||||
## Available Tools
|
||||
|
||||
You can call tools by responding with JSON in this format:
|
||||
{"tool_calls": [{"name": "tool_name", "arguments": {"param": "value"}}]}
|
||||
|
||||
After receiving tool results, provide a natural language response to the user.
|
||||
|
||||
Tools:
|
||||
%s
|
||||
`, strings.Join(toolDefs, "\n"))
|
||||
}
|
||||
|
||||
return a.systemPrompt + toolsSection
|
||||
}
|
||||
|
||||
// buildConversationPrompt builds the conversation history as a prompt
|
||||
func (a *Agent) buildConversationPrompt(session *Session) string {
|
||||
messages := session.GetMessages()
|
||||
var parts []string
|
||||
|
||||
for _, msg := range messages {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", strings.Title(msg.Role), msg.Content))
|
||||
}
|
||||
|
||||
return strings.Join(parts, "\n\n")
|
||||
}
|
||||
|
||||
// parseResponse extracts tool calls and text from AI response
|
||||
func (a *Agent) parseResponse(response string) ([]ToolCall, string, error) {
|
||||
// Try to find JSON tool calls in response
|
||||
// Look for {"tool_calls": [...]} pattern
|
||||
|
||||
var toolCalls []ToolCall
|
||||
textResponse := response
|
||||
|
||||
// Try to parse as JSON
|
||||
if strings.Contains(response, "tool_calls") {
|
||||
// Find JSON block
|
||||
start := strings.Index(response, "{")
|
||||
end := strings.LastIndex(response, "}")
|
||||
|
||||
if start >= 0 && end > start {
|
||||
jsonStr := response[start : end+1]
|
||||
|
||||
var parsed struct {
|
||||
ToolCalls []struct {
|
||||
Name string `json:"name"`
|
||||
Arguments json.RawMessage `json:"arguments"`
|
||||
} `json:"tool_calls"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(jsonStr), &parsed); err == nil {
|
||||
for _, tc := range parsed.ToolCalls {
|
||||
toolCalls = append(toolCalls, ToolCall{
|
||||
Name: tc.Name,
|
||||
Arguments: tc.Arguments,
|
||||
})
|
||||
}
|
||||
|
||||
// Extract text before/after JSON
|
||||
textResponse = strings.TrimSpace(response[:start] + response[end+1:])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return toolCalls, textResponse, nil
|
||||
}
|
||||
|
||||
// executeToolCalls runs the requested tools
|
||||
func (a *Agent) executeToolCalls(ctx context.Context, calls []ToolCall) []ToolResult {
|
||||
a.toolsLock.RLock()
|
||||
defer a.toolsLock.RUnlock()
|
||||
|
||||
var results []ToolResult
|
||||
|
||||
for _, call := range calls {
|
||||
tool, ok := a.tools[call.Name]
|
||||
if !ok {
|
||||
results = append(results, ToolResult{
|
||||
Name: call.Name,
|
||||
Error: fmt.Sprintf("unknown tool: %s", call.Name),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Infof("🔧 Executing tool: %s", call.Name)
|
||||
|
||||
result, err := tool.Execute(ctx, call.Arguments)
|
||||
if err != nil {
|
||||
logger.Errorf("❌ Tool %s failed: %v", call.Name, err)
|
||||
results = append(results, ToolResult{
|
||||
Name: call.Name,
|
||||
Error: err.Error(),
|
||||
})
|
||||
} else {
|
||||
logger.Infof("✅ Tool %s completed", call.Name)
|
||||
results = append(results, ToolResult{
|
||||
Name: call.Name,
|
||||
Result: result,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// ToolCall represents a tool invocation request from the AI
|
||||
type ToolCall struct {
|
||||
Name string `json:"name"`
|
||||
Arguments json.RawMessage `json:"arguments"`
|
||||
}
|
||||
|
||||
// ToolResult represents the result of a tool execution
|
||||
type ToolResult struct {
|
||||
Name string `json:"name"`
|
||||
Result interface{} `json:"result,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// AgentResponse is the final response from the agent
|
||||
type AgentResponse struct {
|
||||
Text string `json:"text"`
|
||||
SessionID string `json:"session_id"`
|
||||
}
|
||||
|
||||
func formatToolCalls(calls []ToolCall) string {
|
||||
var parts []string
|
||||
for _, c := range calls {
|
||||
parts = append(parts, fmt.Sprintf("- %s(%s)", c.Name, string(c.Arguments)))
|
||||
}
|
||||
return strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
func formatToolResults(results []ToolResult) string {
|
||||
var parts []string
|
||||
for _, r := range results {
|
||||
if r.Error != "" {
|
||||
parts = append(parts, fmt.Sprintf("- %s: ERROR: %s", r.Name, r.Error))
|
||||
} else {
|
||||
resultJSON, _ := json.Marshal(r.Result)
|
||||
parts = append(parts, fmt.Sprintf("- %s: %s", r.Name, string(resultJSON)))
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "\n")
|
||||
}
|
||||
312
assistant/context.go
Normal file
312
assistant/context.go
Normal file
@@ -0,0 +1,312 @@
|
||||
// Package assistant - Trading Context Builder
|
||||
// Automatically enriches AI prompts with real-time market and portfolio data
|
||||
package assistant
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"nofx/manager"
|
||||
"nofx/store"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TradingContext holds real-time trading context for AI decision making
|
||||
type TradingContext struct {
|
||||
// Portfolio state
|
||||
TotalEquity float64 `json:"total_equity"`
|
||||
AvailableBalance float64 `json:"available_balance"`
|
||||
UnrealizedPnL float64 `json:"unrealized_pnl"`
|
||||
Positions []PositionSummary `json:"positions"`
|
||||
|
||||
// Market data
|
||||
MarketPrices map[string]float64 `json:"market_prices"`
|
||||
PriceChanges24h map[string]float64 `json:"price_changes_24h"`
|
||||
|
||||
// Trader states
|
||||
ActiveTraders []TraderSummary `json:"active_traders"`
|
||||
|
||||
// Alerts
|
||||
Alerts []Alert `json:"alerts"`
|
||||
|
||||
// Timestamp
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// PositionSummary summarizes a position
|
||||
type PositionSummary struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Side string `json:"side"` // "long" or "short"
|
||||
Size float64 `json:"size"`
|
||||
EntryPrice float64 `json:"entry_price"`
|
||||
MarkPrice float64 `json:"mark_price"`
|
||||
UnrealizedPnL float64 `json:"unrealized_pnl"`
|
||||
PnLPercent float64 `json:"pnl_percent"`
|
||||
Leverage int `json:"leverage"`
|
||||
LiquidationPrice float64 `json:"liquidation_price,omitempty"`
|
||||
TraderID string `json:"trader_id"`
|
||||
TraderName string `json:"trader_name"`
|
||||
}
|
||||
|
||||
// TraderSummary summarizes a trader's state
|
||||
type TraderSummary struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Exchange string `json:"exchange"`
|
||||
IsRunning bool `json:"is_running"`
|
||||
Equity float64 `json:"equity"`
|
||||
PositionCount int `json:"position_count"`
|
||||
TodayPnL float64 `json:"today_pnl,omitempty"`
|
||||
}
|
||||
|
||||
// Alert represents a trading alert
|
||||
type Alert struct {
|
||||
Level string `json:"level"` // "info", "warning", "danger"
|
||||
Type string `json:"type"` // "liquidation_risk", "large_loss", "price_alert", etc.
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// ContextBuilder builds trading context for AI
|
||||
type ContextBuilder struct {
|
||||
traderManager *manager.TraderManager
|
||||
store *store.Store
|
||||
}
|
||||
|
||||
// NewContextBuilder creates a context builder
|
||||
func NewContextBuilder(tm *manager.TraderManager, st *store.Store) *ContextBuilder {
|
||||
return &ContextBuilder{
|
||||
traderManager: tm,
|
||||
store: st,
|
||||
}
|
||||
}
|
||||
|
||||
// BuildContext builds current trading context
|
||||
func (cb *ContextBuilder) BuildContext() *TradingContext {
|
||||
ctx := &TradingContext{
|
||||
MarketPrices: make(map[string]float64),
|
||||
PriceChanges24h: make(map[string]float64),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Get all traders
|
||||
allTraders := cb.traderManager.GetAllTraders()
|
||||
|
||||
for id, trader := range allTraders {
|
||||
summary := TraderSummary{
|
||||
ID: id,
|
||||
Name: trader.GetName(),
|
||||
Exchange: trader.GetExchange(),
|
||||
IsRunning: true, // If in map, it's running
|
||||
}
|
||||
|
||||
// Get account info
|
||||
if accountInfo, err := trader.GetAccountInfo(); err == nil {
|
||||
if equity, ok := accountInfo["total_equity"].(float64); ok {
|
||||
summary.Equity = equity
|
||||
ctx.TotalEquity += equity
|
||||
}
|
||||
if available, ok := accountInfo["available_balance"].(float64); ok {
|
||||
ctx.AvailableBalance += available
|
||||
}
|
||||
}
|
||||
|
||||
// Get positions
|
||||
if positions, err := trader.GetPositions(); err == nil {
|
||||
summary.PositionCount = len(positions)
|
||||
|
||||
for _, pos := range positions {
|
||||
posSummary := cb.parsePosition(pos, id, trader.GetName())
|
||||
if posSummary != nil {
|
||||
ctx.Positions = append(ctx.Positions, *posSummary)
|
||||
ctx.UnrealizedPnL += posSummary.UnrealizedPnL
|
||||
|
||||
// Track market prices
|
||||
ctx.MarketPrices[posSummary.Symbol] = posSummary.MarkPrice
|
||||
|
||||
// Check for alerts
|
||||
cb.checkPositionAlerts(ctx, posSummary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.ActiveTraders = append(ctx.ActiveTraders, summary)
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
// parsePosition parses position data into summary
|
||||
func (cb *ContextBuilder) parsePosition(pos map[string]interface{}, traderID, traderName string) *PositionSummary {
|
||||
summary := &PositionSummary{
|
||||
TraderID: traderID,
|
||||
TraderName: traderName,
|
||||
}
|
||||
|
||||
if symbol, ok := pos["symbol"].(string); ok {
|
||||
summary.Symbol = symbol
|
||||
}
|
||||
if side, ok := pos["side"].(string); ok {
|
||||
summary.Side = strings.ToLower(side)
|
||||
}
|
||||
if size, ok := pos["size"].(float64); ok {
|
||||
summary.Size = size
|
||||
}
|
||||
if entry, ok := pos["entry_price"].(float64); ok {
|
||||
summary.EntryPrice = entry
|
||||
}
|
||||
if mark, ok := pos["mark_price"].(float64); ok {
|
||||
summary.MarkPrice = mark
|
||||
}
|
||||
if pnl, ok := pos["unrealized_pnl"].(float64); ok {
|
||||
summary.UnrealizedPnL = pnl
|
||||
}
|
||||
if lev, ok := pos["leverage"].(int); ok {
|
||||
summary.Leverage = lev
|
||||
}
|
||||
if liq, ok := pos["liquidation_price"].(float64); ok {
|
||||
summary.LiquidationPrice = liq
|
||||
}
|
||||
|
||||
// Calculate PnL percent
|
||||
if summary.EntryPrice > 0 && summary.Size > 0 {
|
||||
if summary.Side == "long" {
|
||||
summary.PnLPercent = ((summary.MarkPrice - summary.EntryPrice) / summary.EntryPrice) * 100 * float64(summary.Leverage)
|
||||
} else {
|
||||
summary.PnLPercent = ((summary.EntryPrice - summary.MarkPrice) / summary.EntryPrice) * 100 * float64(summary.Leverage)
|
||||
}
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
// checkPositionAlerts checks for position-related alerts
|
||||
func (cb *ContextBuilder) checkPositionAlerts(ctx *TradingContext, pos *PositionSummary) {
|
||||
// Liquidation risk alert
|
||||
if pos.LiquidationPrice > 0 && pos.MarkPrice > 0 {
|
||||
var distancePercent float64
|
||||
if pos.Side == "long" {
|
||||
distancePercent = ((pos.MarkPrice - pos.LiquidationPrice) / pos.MarkPrice) * 100
|
||||
} else {
|
||||
distancePercent = ((pos.LiquidationPrice - pos.MarkPrice) / pos.MarkPrice) * 100
|
||||
}
|
||||
|
||||
if distancePercent < 5 {
|
||||
ctx.Alerts = append(ctx.Alerts, Alert{
|
||||
Level: "danger",
|
||||
Type: "liquidation_risk",
|
||||
Message: fmt.Sprintf("⚠️ %s %s仓位距离强平仅 %.1f%%!", pos.Symbol, pos.Side, distancePercent),
|
||||
})
|
||||
} else if distancePercent < 10 {
|
||||
ctx.Alerts = append(ctx.Alerts, Alert{
|
||||
Level: "warning",
|
||||
Type: "liquidation_risk",
|
||||
Message: fmt.Sprintf("⚡ %s %s仓位距离强平 %.1f%%,注意风险", pos.Symbol, pos.Side, distancePercent),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Large loss alert
|
||||
if pos.PnLPercent < -20 {
|
||||
ctx.Alerts = append(ctx.Alerts, Alert{
|
||||
Level: "danger",
|
||||
Type: "large_loss",
|
||||
Message: fmt.Sprintf("📉 %s %s仓位亏损 %.1f%%,考虑止损", pos.Symbol, pos.Side, pos.PnLPercent),
|
||||
})
|
||||
} else if pos.PnLPercent < -10 {
|
||||
ctx.Alerts = append(ctx.Alerts, Alert{
|
||||
Level: "warning",
|
||||
Type: "large_loss",
|
||||
Message: fmt.Sprintf("📉 %s %s仓位亏损 %.1f%%", pos.Symbol, pos.Side, pos.PnLPercent),
|
||||
})
|
||||
}
|
||||
|
||||
// Large profit - consider taking profit
|
||||
if pos.PnLPercent > 50 {
|
||||
ctx.Alerts = append(ctx.Alerts, Alert{
|
||||
Level: "info",
|
||||
Type: "large_profit",
|
||||
Message: fmt.Sprintf("📈 %s %s仓位盈利 %.1f%%,考虑部分止盈", pos.Symbol, pos.Side, pos.PnLPercent),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// FormatContextForPrompt formats context as text for AI prompt injection
|
||||
func (ctx *TradingContext) FormatContextForPrompt() string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString("\n\n---\n## 📊 当前交易状态 (实时)\n\n")
|
||||
|
||||
// Portfolio summary
|
||||
sb.WriteString(fmt.Sprintf("**总权益:** $%.2f | **可用余额:** $%.2f | **未实现盈亏:** $%.2f\n\n",
|
||||
ctx.TotalEquity, ctx.AvailableBalance, ctx.UnrealizedPnL))
|
||||
|
||||
// Alerts (high priority)
|
||||
if len(ctx.Alerts) > 0 {
|
||||
sb.WriteString("### ⚠️ 警报\n")
|
||||
for _, alert := range ctx.Alerts {
|
||||
sb.WriteString(fmt.Sprintf("- %s\n", alert.Message))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// Active positions
|
||||
if len(ctx.Positions) > 0 {
|
||||
sb.WriteString("### 📈 持仓\n")
|
||||
sb.WriteString("| 交易对 | 方向 | 数量 | 入场价 | 现价 | 盈亏 | 盈亏% | 杠杆 | 交易员 |\n")
|
||||
sb.WriteString("|--------|------|------|--------|------|------|-------|------|--------|\n")
|
||||
for _, pos := range ctx.Positions {
|
||||
pnlEmoji := "🟢"
|
||||
if pos.UnrealizedPnL < 0 {
|
||||
pnlEmoji = "🔴"
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("| %s | %s | %.4f | %.2f | %.2f | %s$%.2f | %.1f%% | %dx | %s |\n",
|
||||
pos.Symbol, pos.Side, pos.Size, pos.EntryPrice, pos.MarkPrice,
|
||||
pnlEmoji, pos.UnrealizedPnL, pos.PnLPercent, pos.Leverage, pos.TraderName))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
} else {
|
||||
sb.WriteString("### 📈 持仓\n无持仓\n\n")
|
||||
}
|
||||
|
||||
// Active traders
|
||||
if len(ctx.ActiveTraders) > 0 {
|
||||
sb.WriteString("### 🤖 运行中的交易员\n")
|
||||
for _, t := range ctx.ActiveTraders {
|
||||
status := "✅ 运行中"
|
||||
if !t.IsRunning {
|
||||
status = "❌ 已停止"
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("- **%s** (%s) %s | 权益: $%.2f | 持仓: %d\n",
|
||||
t.Name, t.Exchange, status, t.Equity, t.PositionCount))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("*数据更新时间: %s*\n---\n", ctx.UpdatedAt.Format("2006-01-02 15:04:05")))
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// GetTopSymbols returns symbols with positions for market data queries
|
||||
func (ctx *TradingContext) GetTopSymbols() []string {
|
||||
symbolSet := make(map[string]bool)
|
||||
for _, pos := range ctx.Positions {
|
||||
symbolSet[pos.Symbol] = true
|
||||
}
|
||||
|
||||
// Always include major pairs
|
||||
symbolSet["BTCUSDT"] = true
|
||||
symbolSet["ETHUSDT"] = true
|
||||
|
||||
symbols := make([]string, 0, len(symbolSet))
|
||||
for s := range symbolSet {
|
||||
symbols = append(symbols, s)
|
||||
}
|
||||
return symbols
|
||||
}
|
||||
|
||||
// EnrichWithMarketData adds market data to context
|
||||
// Note: Market prices are already populated from position data
|
||||
func (cb *ContextBuilder) EnrichWithMarketData(ctx *TradingContext, symbols []string) {
|
||||
// Market prices are populated from position mark prices
|
||||
// Additional market data enrichment can be added here in the future
|
||||
}
|
||||
200
assistant/monitor.go
Normal file
200
assistant/monitor.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package assistant
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"nofx/logger"
|
||||
"nofx/manager"
|
||||
"nofx/store"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Monitor provides proactive monitoring and alerts
|
||||
type Monitor struct {
|
||||
traderManager *manager.TraderManager
|
||||
store *store.Store
|
||||
contextBuilder *ContextBuilder
|
||||
|
||||
// Alert callbacks
|
||||
alertCallbacks []func(Alert)
|
||||
callbackMu sync.RWMutex
|
||||
|
||||
// State
|
||||
running bool
|
||||
stopChan chan struct{}
|
||||
interval time.Duration
|
||||
|
||||
// Last known state for change detection
|
||||
lastPositions map[string]PositionSummary
|
||||
lastAlerts map[string]time.Time // Prevent alert spam
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewMonitor creates a new trading monitor
|
||||
func NewMonitor(tm *manager.TraderManager, st *store.Store) *Monitor {
|
||||
return &Monitor{
|
||||
traderManager: tm,
|
||||
store: st,
|
||||
contextBuilder: NewContextBuilder(tm, st),
|
||||
stopChan: make(chan struct{}),
|
||||
interval: 30 * time.Second, // Check every 30 seconds
|
||||
lastPositions: make(map[string]PositionSummary),
|
||||
lastAlerts: make(map[string]time.Time),
|
||||
}
|
||||
}
|
||||
|
||||
// OnAlert registers an alert callback
|
||||
func (m *Monitor) OnAlert(callback func(Alert)) {
|
||||
m.callbackMu.Lock()
|
||||
defer m.callbackMu.Unlock()
|
||||
m.alertCallbacks = append(m.alertCallbacks, callback)
|
||||
}
|
||||
|
||||
// Start starts the monitor
|
||||
func (m *Monitor) Start() {
|
||||
m.mu.Lock()
|
||||
if m.running {
|
||||
m.mu.Unlock()
|
||||
return
|
||||
}
|
||||
m.running = true
|
||||
m.stopChan = make(chan struct{})
|
||||
m.mu.Unlock()
|
||||
|
||||
logger.Info("🔍 Starting trading monitor...")
|
||||
|
||||
go m.monitorLoop()
|
||||
}
|
||||
|
||||
// Stop stops the monitor
|
||||
func (m *Monitor) Stop() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if !m.running {
|
||||
return
|
||||
}
|
||||
|
||||
m.running = false
|
||||
close(m.stopChan)
|
||||
logger.Info("🔍 Trading monitor stopped")
|
||||
}
|
||||
|
||||
// monitorLoop is the main monitoring loop
|
||||
func (m *Monitor) monitorLoop() {
|
||||
ticker := time.NewTicker(m.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Initial check
|
||||
m.checkAndAlert()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
m.checkAndAlert()
|
||||
case <-m.stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkAndAlert checks positions and sends alerts
|
||||
func (m *Monitor) checkAndAlert() {
|
||||
ctx := m.contextBuilder.BuildContext()
|
||||
|
||||
// Process built-in alerts from context
|
||||
for _, alert := range ctx.Alerts {
|
||||
m.sendAlertIfNew(alert)
|
||||
}
|
||||
|
||||
// Check for position changes
|
||||
m.checkPositionChanges(ctx)
|
||||
|
||||
// Check for new large movements
|
||||
m.checkMarketMovements(ctx)
|
||||
}
|
||||
|
||||
// checkPositionChanges detects significant position changes
|
||||
func (m *Monitor) checkPositionChanges(ctx *TradingContext) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
currentPositions := make(map[string]PositionSummary)
|
||||
|
||||
for _, pos := range ctx.Positions {
|
||||
key := fmt.Sprintf("%s_%s_%s", pos.TraderID, pos.Symbol, pos.Side)
|
||||
currentPositions[key] = pos
|
||||
|
||||
// Check if this is a new position
|
||||
if _, existed := m.lastPositions[key]; !existed {
|
||||
m.sendAlert(Alert{
|
||||
Level: "info",
|
||||
Type: "new_position",
|
||||
Message: fmt.Sprintf("📍 新开仓位: %s %s %.4f @ %.2f (%dx)",
|
||||
pos.Symbol, pos.Side, pos.Size, pos.EntryPrice, pos.Leverage),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Check for closed positions
|
||||
for key, oldPos := range m.lastPositions {
|
||||
if _, exists := currentPositions[key]; !exists {
|
||||
m.sendAlert(Alert{
|
||||
Level: "info",
|
||||
Type: "position_closed",
|
||||
Message: fmt.Sprintf("📍 仓位已平: %s %s (入场价: %.2f)",
|
||||
oldPos.Symbol, oldPos.Side, oldPos.EntryPrice),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
m.lastPositions = currentPositions
|
||||
}
|
||||
|
||||
// checkMarketMovements checks for significant market movements
|
||||
func (m *Monitor) checkMarketMovements(ctx *TradingContext) {
|
||||
// This could be expanded to check price movements
|
||||
// For now, we rely on the context builder's alerts
|
||||
}
|
||||
|
||||
// sendAlertIfNew sends an alert only if it's new (avoid spam)
|
||||
func (m *Monitor) sendAlertIfNew(alert Alert) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
key := fmt.Sprintf("%s_%s", alert.Type, alert.Message)
|
||||
|
||||
// Check if we sent this alert recently (within 5 minutes)
|
||||
if lastSent, ok := m.lastAlerts[key]; ok {
|
||||
if time.Since(lastSent) < 5*time.Minute {
|
||||
return // Skip, already sent recently
|
||||
}
|
||||
}
|
||||
|
||||
m.lastAlerts[key] = time.Now()
|
||||
m.sendAlert(alert)
|
||||
}
|
||||
|
||||
// sendAlert sends alert to all registered callbacks
|
||||
func (m *Monitor) sendAlert(alert Alert) {
|
||||
m.callbackMu.RLock()
|
||||
callbacks := make([]func(Alert), len(m.alertCallbacks))
|
||||
copy(callbacks, m.alertCallbacks)
|
||||
m.callbackMu.RUnlock()
|
||||
|
||||
for _, cb := range callbacks {
|
||||
go cb(alert)
|
||||
}
|
||||
}
|
||||
|
||||
// GetCurrentContext returns the current trading context
|
||||
func (m *Monitor) GetCurrentContext() *TradingContext {
|
||||
return m.contextBuilder.BuildContext()
|
||||
}
|
||||
|
||||
// SetInterval sets the monitoring interval
|
||||
func (m *Monitor) SetInterval(d time.Duration) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.interval = d
|
||||
}
|
||||
117
assistant/prompts.go
Normal file
117
assistant/prompts.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package assistant
|
||||
|
||||
// DefaultTradingSystemPrompt returns the default system prompt for trading assistant
|
||||
func DefaultTradingSystemPrompt() string {
|
||||
return `# NOFX Trading Assistant
|
||||
|
||||
You are an expert AI trading assistant powered by NOFX - an advanced AI-powered trading system.
|
||||
|
||||
## Your Capabilities
|
||||
|
||||
1. **Account Management**
|
||||
- Check balances across multiple exchanges
|
||||
- View current positions and P&L
|
||||
- Monitor portfolio performance
|
||||
|
||||
2. **Trading Operations**
|
||||
- Execute trades (open/close positions)
|
||||
- Manage stop-loss and take-profit orders
|
||||
- Adjust leverage and margin settings
|
||||
|
||||
3. **AI Traders Management**
|
||||
- Start/stop AI traders
|
||||
- Monitor AI trader performance
|
||||
- Configure trading strategies
|
||||
|
||||
4. **Strategy & Analysis**
|
||||
- Create and modify trading strategies
|
||||
- Initiate AI debate sessions for market analysis
|
||||
- Backtest strategies on historical data
|
||||
|
||||
5. **Market Intelligence**
|
||||
- Get real-time prices and market data
|
||||
- Analyze market conditions
|
||||
- Track open interest and funding rates
|
||||
|
||||
## Guidelines
|
||||
|
||||
1. **Safety First**: Always confirm with the user before executing trades or making significant changes
|
||||
2. **Be Precise**: When dealing with numbers, be exact - trading involves real money
|
||||
3. **Explain Reasoning**: Help users understand your analysis and recommendations
|
||||
4. **Risk Awareness**: Always remind users about the risks involved in trading
|
||||
5. **Proactive Monitoring**: Alert users to important position changes or market movements
|
||||
|
||||
## Response Style
|
||||
|
||||
- Be concise but thorough
|
||||
- Use tables for data when appropriate
|
||||
- Include relevant metrics (P&L, ROI, etc.)
|
||||
- Provide actionable insights, not just data dumps
|
||||
- Support both English and Chinese (respond in the user's language)
|
||||
|
||||
## Important Notes
|
||||
|
||||
- Never share API keys or sensitive credentials
|
||||
- Always use proper position sizing based on user's risk tolerance
|
||||
- Warn users about high-risk operations (high leverage, large positions)
|
||||
|
||||
Remember: You are a professional trading assistant. Users trust you with their trading operations. Be accurate, be helpful, and be responsible.`
|
||||
}
|
||||
|
||||
// ChineseSystemPrompt returns Chinese version of the system prompt
|
||||
func ChineseSystemPrompt() string {
|
||||
return `# NOFX 交易助手
|
||||
|
||||
你是一个由 NOFX 驱动的专业 AI 交易助手 - 一个先进的 AI 驱动交易系统。
|
||||
|
||||
## 你的能力
|
||||
|
||||
1. **账户管理**
|
||||
- 查询多交易所余额
|
||||
- 查看当前持仓和盈亏
|
||||
- 监控投资组合表现
|
||||
|
||||
2. **交易操作**
|
||||
- 执行交易(开仓/平仓)
|
||||
- 管理止损止盈订单
|
||||
- 调整杠杆和保证金设置
|
||||
|
||||
3. **AI 交易员管理**
|
||||
- 启动/停止 AI 交易员
|
||||
- 监控 AI 交易员表现
|
||||
- 配置交易策略
|
||||
|
||||
4. **策略与分析**
|
||||
- 创建和修改交易策略
|
||||
- 发起 AI 辩论会议进行市场分析
|
||||
- 回测历史数据
|
||||
|
||||
5. **市场情报**
|
||||
- 获取实时价格和市场数据
|
||||
- 分析市场状况
|
||||
- 跟踪持仓量和资金费率
|
||||
|
||||
## 行为准则
|
||||
|
||||
1. **安全第一**:执行交易或重大操作前,务必与用户确认
|
||||
2. **精确无误**:涉及数字时必须精确 - 交易涉及真金白银
|
||||
3. **解释逻辑**:帮助用户理解你的分析和建议
|
||||
4. **风险意识**:始终提醒用户交易风险
|
||||
5. **主动监控**:及时提醒用户重要的仓位变化或市场波动
|
||||
|
||||
## 回复风格
|
||||
|
||||
- 简洁但全面
|
||||
- 适当使用表格展示数据
|
||||
- 包含相关指标(盈亏、收益率等)
|
||||
- 提供可操作的见解,而非单纯的数据罗列
|
||||
- 支持中英文(根据用户使用的语言回复)
|
||||
|
||||
## 重要提示
|
||||
|
||||
- 永远不要分享 API 密钥或敏感凭证
|
||||
- 根据用户的风险承受能力进行合理的仓位管理
|
||||
- 对高风险操作(高杠杆、大仓位)发出警告
|
||||
|
||||
记住:你是专业的交易助手。用户将交易操作托付于你。准确、有用、负责。`
|
||||
}
|
||||
122
assistant/session.go
Normal file
122
assistant/session.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package assistant
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Message represents a single message in conversation
|
||||
type Message struct {
|
||||
Role string `json:"role"` // "user", "assistant", "system", "tool"
|
||||
Content string `json:"content"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
|
||||
// For tool messages
|
||||
ToolName string `json:"tool_name,omitempty"`
|
||||
ToolResult interface{} `json:"tool_result,omitempty"`
|
||||
}
|
||||
|
||||
// Session represents a conversation session with memory
|
||||
type Session struct {
|
||||
ID string `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// User info
|
||||
UserID string `json:"user_id"`
|
||||
UserName string `json:"user_name"`
|
||||
Platform string `json:"platform"` // "telegram", "web", etc.
|
||||
|
||||
// Conversation history
|
||||
messages []Message
|
||||
maxMessages int
|
||||
mu sync.RWMutex
|
||||
|
||||
// Custom metadata
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
// NewSession creates a new conversation session
|
||||
func NewSession(id string, maxMessages int) *Session {
|
||||
return &Session{
|
||||
ID: id,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
messages: make([]Message, 0),
|
||||
maxMessages: maxMessages,
|
||||
Metadata: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// AddMessage adds a message to the session
|
||||
func (s *Session) AddMessage(msg Message) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.messages = append(s.messages, msg)
|
||||
s.UpdatedAt = time.Now()
|
||||
|
||||
// Trim old messages if exceeding max
|
||||
if len(s.messages) > s.maxMessages {
|
||||
// Keep the most recent messages
|
||||
s.messages = s.messages[len(s.messages)-s.maxMessages:]
|
||||
}
|
||||
}
|
||||
|
||||
// GetMessages returns a copy of all messages
|
||||
func (s *Session) GetMessages() []Message {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
result := make([]Message, len(s.messages))
|
||||
copy(result, s.messages)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetRecentMessages returns the N most recent messages
|
||||
func (s *Session) GetRecentMessages(n int) []Message {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if n >= len(s.messages) {
|
||||
result := make([]Message, len(s.messages))
|
||||
copy(result, s.messages)
|
||||
return result
|
||||
}
|
||||
|
||||
result := make([]Message, n)
|
||||
copy(result, s.messages[len(s.messages)-n:])
|
||||
return result
|
||||
}
|
||||
|
||||
// Clear removes all messages from the session
|
||||
func (s *Session) Clear() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.messages = make([]Message, 0)
|
||||
s.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// SetUserInfo sets user information
|
||||
func (s *Session) SetUserInfo(userID, userName, platform string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.UserID = userID
|
||||
s.UserName = userName
|
||||
s.Platform = platform
|
||||
}
|
||||
|
||||
// SetMetadata sets a metadata value
|
||||
func (s *Session) SetMetadata(key string, value interface{}) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.Metadata[key] = value
|
||||
}
|
||||
|
||||
// GetMetadata gets a metadata value
|
||||
func (s *Session) GetMetadata(key string) (interface{}, bool) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
v, ok := s.Metadata[key]
|
||||
return v, ok
|
||||
}
|
||||
216
assistant/smart_agent.go
Normal file
216
assistant/smart_agent.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package assistant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"nofx/logger"
|
||||
"nofx/manager"
|
||||
"nofx/mcp"
|
||||
"nofx/store"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SmartAgent is an enhanced AI agent with trading context awareness
|
||||
type SmartAgent struct {
|
||||
*Agent
|
||||
contextBuilder *ContextBuilder
|
||||
monitor *Monitor
|
||||
|
||||
// Auto-inject context into prompts
|
||||
autoInjectContext bool
|
||||
}
|
||||
|
||||
// NewSmartAgent creates a new smart trading agent
|
||||
func NewSmartAgent(aiClient mcp.AIClient, config AgentConfig, tm *manager.TraderManager, st *store.Store) *SmartAgent {
|
||||
baseAgent := NewAgent(aiClient, config)
|
||||
baseAgent.SetSystemPrompt(SmartTradingPrompt())
|
||||
|
||||
contextBuilder := NewContextBuilder(tm, st)
|
||||
monitor := NewMonitor(tm, st)
|
||||
|
||||
return &SmartAgent{
|
||||
Agent: baseAgent,
|
||||
contextBuilder: contextBuilder,
|
||||
monitor: monitor,
|
||||
autoInjectContext: true,
|
||||
}
|
||||
}
|
||||
|
||||
// SetAutoInjectContext enables/disables automatic context injection
|
||||
func (sa *SmartAgent) SetAutoInjectContext(enabled bool) {
|
||||
sa.autoInjectContext = enabled
|
||||
}
|
||||
|
||||
// StartMonitor starts the background monitor
|
||||
func (sa *SmartAgent) StartMonitor() {
|
||||
sa.monitor.Start()
|
||||
}
|
||||
|
||||
// StopMonitor stops the background monitor
|
||||
func (sa *SmartAgent) StopMonitor() {
|
||||
sa.monitor.Stop()
|
||||
}
|
||||
|
||||
// OnAlert registers an alert callback
|
||||
func (sa *SmartAgent) OnAlert(callback func(Alert)) {
|
||||
sa.monitor.OnAlert(callback)
|
||||
}
|
||||
|
||||
// Chat processes a message with smart context injection
|
||||
func (sa *SmartAgent) Chat(ctx context.Context, sessionID string, userMessage string) (*AgentResponse, error) {
|
||||
session := sa.GetSession(sessionID)
|
||||
|
||||
// Add user message to history
|
||||
session.AddMessage(Message{
|
||||
Role: "user",
|
||||
Content: userMessage,
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
|
||||
// Build system prompt with tools
|
||||
systemPrompt := sa.buildSmartSystemPrompt()
|
||||
|
||||
// Build conversation prompt with context injection
|
||||
conversationPrompt := sa.buildSmartConversationPrompt(session, userMessage)
|
||||
|
||||
// Agent loop
|
||||
var finalResponse string
|
||||
toolCallCount := 0
|
||||
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
if toolCallCount >= sa.config.MaxToolCalls {
|
||||
logger.Warnf("⚠️ Max tool calls reached (%d)", sa.config.MaxToolCalls)
|
||||
break
|
||||
}
|
||||
|
||||
response, err := sa.aiClient.CallWithMessages(systemPrompt, conversationPrompt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("AI call failed: %w", err)
|
||||
}
|
||||
|
||||
toolCalls, textResponse, err := sa.parseResponse(response)
|
||||
if err != nil {
|
||||
finalResponse = response
|
||||
break
|
||||
}
|
||||
|
||||
if len(toolCalls) == 0 {
|
||||
finalResponse = textResponse
|
||||
break
|
||||
}
|
||||
|
||||
// Execute tool calls
|
||||
toolResults := sa.executeToolCalls(ctx, toolCalls)
|
||||
toolCallCount += len(toolCalls)
|
||||
|
||||
// Add results to conversation
|
||||
conversationPrompt += fmt.Sprintf("\n\nAssistant called tools:\n%s\n\nTool results:\n%s\n\nBased on the tool results, provide a helpful response:",
|
||||
formatToolCalls(toolCalls),
|
||||
formatToolResults(toolResults))
|
||||
|
||||
if textResponse != "" {
|
||||
finalResponse = textResponse
|
||||
}
|
||||
}
|
||||
|
||||
// Add response to history
|
||||
session.AddMessage(Message{
|
||||
Role: "assistant",
|
||||
Content: finalResponse,
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
|
||||
return &AgentResponse{
|
||||
Text: finalResponse,
|
||||
SessionID: sessionID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// buildSmartSystemPrompt builds system prompt with tools
|
||||
func (sa *SmartAgent) buildSmartSystemPrompt() string {
|
||||
sa.toolsLock.RLock()
|
||||
defer sa.toolsLock.RUnlock()
|
||||
|
||||
var toolDefs []string
|
||||
for _, tool := range sa.tools {
|
||||
toolDef := fmt.Sprintf(`- **%s**: %s
|
||||
Parameters: %s`, tool.Name(), tool.Description(), tool.ParameterSchema())
|
||||
toolDefs = append(toolDefs, toolDef)
|
||||
}
|
||||
|
||||
toolsSection := ""
|
||||
if len(toolDefs) > 0 {
|
||||
toolsSection = fmt.Sprintf(`
|
||||
|
||||
## 可用工具
|
||||
|
||||
调用工具时,使用以下 JSON 格式:
|
||||
{"tool_calls": [{"name": "工具名", "arguments": {"参数": "值"}}]}
|
||||
|
||||
收到工具结果后,用自然语言回复用户。
|
||||
|
||||
可用工具:
|
||||
%s
|
||||
`, strings.Join(toolDefs, "\n"))
|
||||
}
|
||||
|
||||
return sa.systemPrompt + toolsSection
|
||||
}
|
||||
|
||||
// buildSmartConversationPrompt builds conversation with context injection
|
||||
func (sa *SmartAgent) buildSmartConversationPrompt(session *Session, currentMessage string) string {
|
||||
var sb strings.Builder
|
||||
|
||||
// Inject current trading context if enabled
|
||||
if sa.autoInjectContext {
|
||||
tradingCtx := sa.contextBuilder.BuildContext()
|
||||
sb.WriteString(tradingCtx.FormatContextForPrompt())
|
||||
}
|
||||
|
||||
// Add conversation history
|
||||
messages := session.GetMessages()
|
||||
for _, msg := range messages {
|
||||
sb.WriteString(fmt.Sprintf("\n%s: %s\n", strings.Title(msg.Role), msg.Content))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// QuickStatus returns a quick status summary
|
||||
func (sa *SmartAgent) QuickStatus() string {
|
||||
ctx := sa.contextBuilder.BuildContext()
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("📊 **交易状态概览**\n\n")
|
||||
|
||||
sb.WriteString(fmt.Sprintf("💰 总权益: $%.2f\n", ctx.TotalEquity))
|
||||
sb.WriteString(fmt.Sprintf("💵 可用余额: $%.2f\n", ctx.AvailableBalance))
|
||||
|
||||
if ctx.UnrealizedPnL >= 0 {
|
||||
sb.WriteString(fmt.Sprintf("📈 未实现盈亏: 🟢 +$%.2f\n", ctx.UnrealizedPnL))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("📉 未实现盈亏: 🔴 $%.2f\n", ctx.UnrealizedPnL))
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("📍 持仓数: %d\n", len(ctx.Positions)))
|
||||
sb.WriteString(fmt.Sprintf("🤖 运行交易员: %d\n", len(ctx.ActiveTraders)))
|
||||
|
||||
if len(ctx.Alerts) > 0 {
|
||||
sb.WriteString("\n⚠️ **警报**\n")
|
||||
for _, alert := range ctx.Alerts {
|
||||
sb.WriteString(fmt.Sprintf("- %s\n", alert.Message))
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// GetTradingContext returns current trading context
|
||||
func (sa *SmartAgent) GetTradingContext() *TradingContext {
|
||||
return sa.contextBuilder.BuildContext()
|
||||
}
|
||||
115
assistant/smart_prompts.go
Normal file
115
assistant/smart_prompts.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package assistant
|
||||
|
||||
import "fmt"
|
||||
|
||||
// SmartTradingPrompt returns an enhanced system prompt with trading intelligence
|
||||
func SmartTradingPrompt() string {
|
||||
return `# 🧠 NOFX 智能交易助手
|
||||
|
||||
你是一个专业的 AI 交易助手,具备以下能力:
|
||||
|
||||
## 核心能力
|
||||
|
||||
### 1. 智能分析
|
||||
- 分析用户意图,理解交易需求
|
||||
- 在执行交易前,主动评估风险
|
||||
- 结合市场数据给出建议
|
||||
|
||||
### 2. 主动提醒
|
||||
- 发现持仓风险时主动警告
|
||||
- 大额亏损时建议止损
|
||||
- 接近强平时紧急提醒
|
||||
|
||||
### 3. 专业建议
|
||||
- 根据仓位情况建议操作
|
||||
- 评估杠杆和仓位大小是否合理
|
||||
- 提供入场/出场时机建议
|
||||
|
||||
## 交易原则
|
||||
|
||||
1. **安全第一**:任何交易操作前必须确认,高风险操作要多次确认
|
||||
2. **风险控制**:
|
||||
- 单笔交易不超过总资金的 10%
|
||||
- 杠杆建议:BTC/ETH ≤10x,山寨币 ≤5x
|
||||
- 发现强平风险立即警告
|
||||
3. **理性决策**:不鼓励情绪化交易,亏损时建议冷静
|
||||
|
||||
## 回复风格
|
||||
|
||||
- 简洁专业,像交易员一样说话
|
||||
- 数据说话,给出具体数字
|
||||
- 风险提示放在显眼位置
|
||||
- 支持中英文,根据用户语言回复
|
||||
|
||||
## 工具使用策略
|
||||
|
||||
当用户问到持仓、余额时:
|
||||
1. 先调用 list_traders 获取交易员列表
|
||||
2. 对运行中的交易员调用 get_balance 和 get_positions
|
||||
3. 汇总数据后清晰展示
|
||||
|
||||
当用户想交易时:
|
||||
1. 先获取当前持仓和余额
|
||||
2. 评估这笔交易的风险
|
||||
3. 明确告知风险后请求确认
|
||||
4. 确认后执行交易
|
||||
|
||||
当用户问市场行情时:
|
||||
1. 获取相关币种价格
|
||||
2. 结合持仓情况分析
|
||||
3. 给出操作建议(但声明不构成投资建议)
|
||||
|
||||
## 重要:响应格式
|
||||
|
||||
- 持仓展示用表格
|
||||
- 重要警告用 ⚠️ 标注
|
||||
- 盈利用 🟢,亏损用 🔴
|
||||
- 操作建议用列表
|
||||
|
||||
记住:你的目标是帮助用户更好地管理交易,而不是鼓励频繁交易。稳健盈利比追求高收益更重要。`
|
||||
}
|
||||
|
||||
// RiskAssessmentPrompt returns a prompt for risk assessment before trades
|
||||
func RiskAssessmentPrompt(action, symbol string, quantity, leverage float64, currentBalance, currentPositions string) string {
|
||||
return fmt.Sprintf(`## 交易风险评估
|
||||
|
||||
请评估以下交易的风险:
|
||||
|
||||
**操作**: %s %s
|
||||
**数量**: %.4f
|
||||
**杠杆**: %.0fx
|
||||
|
||||
**当前账户状态**:
|
||||
%s
|
||||
|
||||
**当前持仓**:
|
||||
%s
|
||||
|
||||
请分析:
|
||||
1. 这笔交易是否合理?
|
||||
2. 仓位大小是否过大?
|
||||
3. 杠杆是否过高?
|
||||
4. 有什么潜在风险?
|
||||
5. 你的建议是什么?
|
||||
|
||||
如果风险过高,请明确警告用户。`, action, symbol, quantity, leverage, currentBalance, currentPositions)
|
||||
}
|
||||
|
||||
// MarketAnalysisPrompt returns a prompt for market analysis
|
||||
func MarketAnalysisPrompt(symbol string, priceData, positionData string) string {
|
||||
return fmt.Sprintf(`## %s 市场分析
|
||||
|
||||
**价格数据**:
|
||||
%s
|
||||
|
||||
**相关持仓**:
|
||||
%s
|
||||
|
||||
请分析:
|
||||
1. 当前价格趋势
|
||||
2. 关键支撑/阻力位
|
||||
3. 持仓建议(继续持有/加仓/减仓/平仓)
|
||||
4. 风险提示
|
||||
|
||||
注:这是基于有限数据的分析,不构成投资建议。`, symbol, priceData, positionData)
|
||||
}
|
||||
382
assistant/strategy_builder.go
Normal file
382
assistant/strategy_builder.go
Normal file
@@ -0,0 +1,382 @@
|
||||
// Package assistant - Intelligent Strategy Builder
|
||||
// Allows users to create powerful, flexible trading strategies through natural language
|
||||
package assistant
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"nofx/store"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// StrategyType defines the type of trading strategy
|
||||
type StrategyType string
|
||||
|
||||
const (
|
||||
StrategyTypeAI StrategyType = "ai" // AI decides everything
|
||||
StrategyTypeTrend StrategyType = "trend" // Trend following
|
||||
StrategyTypeMeanRevert StrategyType = "mean_revert" // Mean reversion
|
||||
StrategyTypeGrid StrategyType = "grid" // Grid trading
|
||||
StrategyTypeDCA StrategyType = "dca" // Dollar cost averaging
|
||||
StrategyTypeBreakout StrategyType = "breakout" // Breakout trading
|
||||
StrategyTypeArbitrage StrategyType = "arbitrage" // Cross-exchange arbitrage
|
||||
StrategyTypeCustom StrategyType = "custom" // Custom rules
|
||||
)
|
||||
|
||||
// SmartStrategy represents a user-defined trading strategy
|
||||
type SmartStrategy struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Type StrategyType `json:"type"`
|
||||
|
||||
// Trading pairs
|
||||
Symbols []string `json:"symbols"` // e.g., ["BTCUSDT", "ETHUSDT"]
|
||||
SymbolMode string `json:"symbol_mode"` // "static", "ai_select", "top_volume", "top_oi"
|
||||
MaxSymbols int `json:"max_symbols"` // Max symbols to trade simultaneously
|
||||
|
||||
// Entry conditions
|
||||
EntryRules []Rule `json:"entry_rules"`
|
||||
EntryMode string `json:"entry_mode"` // "any" (OR) or "all" (AND)
|
||||
|
||||
// Exit conditions
|
||||
ExitRules []Rule `json:"exit_rules"`
|
||||
TakeProfit *float64 `json:"take_profit"` // TP percentage
|
||||
StopLoss *float64 `json:"stop_loss"` // SL percentage
|
||||
TrailingStop *float64 `json:"trailing_stop"` // Trailing stop percentage
|
||||
|
||||
// Position sizing
|
||||
PositionSize PositionSizeConfig `json:"position_size"`
|
||||
MaxPositions int `json:"max_positions"` // Max concurrent positions
|
||||
MaxPerSymbol int `json:"max_per_symbol"` // Max positions per symbol
|
||||
|
||||
// Risk management
|
||||
RiskConfig RiskConfig `json:"risk_config"`
|
||||
|
||||
// Leverage settings
|
||||
LeverageConfig LeverageConfig `json:"leverage_config"`
|
||||
|
||||
// Time settings
|
||||
TimeConfig TimeConfig `json:"time_config"`
|
||||
|
||||
// AI enhancement
|
||||
AIConfig AIStrategyConfig `json:"ai_config"`
|
||||
|
||||
// Metadata
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
CreatedBy string `json:"created_by"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Performance *StrategyPerformance `json:"performance,omitempty"`
|
||||
}
|
||||
|
||||
// Rule represents a trading rule/condition
|
||||
type Rule struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"` // "indicator", "price", "time", "volume", "ai", "custom"
|
||||
Indicator string `json:"indicator"` // e.g., "RSI", "MACD", "EMA"
|
||||
Condition string `json:"condition"` // e.g., "crosses_above", "greater_than", "less_than"
|
||||
Value interface{} `json:"value"` // The value to compare against
|
||||
Timeframe string `json:"timeframe"` // e.g., "1h", "4h", "1d"
|
||||
Weight float64 `json:"weight"` // Weight for scoring (0-1)
|
||||
Description string `json:"description"` // Human readable description
|
||||
}
|
||||
|
||||
// PositionSizeConfig defines how to size positions
|
||||
type PositionSizeConfig struct {
|
||||
Mode string `json:"mode"` // "fixed", "percent", "risk_based", "kelly"
|
||||
FixedAmount float64 `json:"fixed_amount"` // Fixed USDT amount
|
||||
PercentOfEquity float64 `json:"percent_of_equity"` // Percentage of total equity
|
||||
RiskPerTrade float64 `json:"risk_per_trade"` // Max risk per trade (%)
|
||||
MaxSingleTrade float64 `json:"max_single_trade"` // Max single trade size (USDT)
|
||||
}
|
||||
|
||||
// RiskConfig defines risk management rules
|
||||
type RiskConfig struct {
|
||||
MaxDrawdown float64 `json:"max_drawdown"` // Max drawdown before stopping (%)
|
||||
MaxDailyLoss float64 `json:"max_daily_loss"` // Max daily loss (%)
|
||||
MaxOpenRisk float64 `json:"max_open_risk"` // Max total open risk (%)
|
||||
CooldownAfterLoss int `json:"cooldown_after_loss"` // Minutes to wait after a loss
|
||||
RequireConfirmation bool `json:"require_confirmation"` // Require user confirmation for trades
|
||||
EmergencyStopLoss float64 `json:"emergency_stop_loss"` // Emergency SL for all positions (%)
|
||||
}
|
||||
|
||||
// LeverageConfig defines leverage settings
|
||||
type LeverageConfig struct {
|
||||
Mode string `json:"mode"` // "fixed", "dynamic", "per_symbol"
|
||||
DefaultLeverage int `json:"default_leverage"`
|
||||
MaxLeverage int `json:"max_leverage"`
|
||||
PerSymbol map[string]int `json:"per_symbol"` // Symbol-specific leverage
|
||||
PerVolatility []VolatilityLever `json:"per_volatility"` // Volatility-based leverage
|
||||
}
|
||||
|
||||
// VolatilityLever defines leverage based on volatility
|
||||
type VolatilityLever struct {
|
||||
MaxVolatility float64 `json:"max_volatility"` // ATR percentage threshold
|
||||
Leverage int `json:"leverage"`
|
||||
}
|
||||
|
||||
// TimeConfig defines time-based settings
|
||||
type TimeConfig struct {
|
||||
TradingHours []TimeRange `json:"trading_hours"` // When to trade
|
||||
AvoidNews bool `json:"avoid_news"` // Avoid major news events
|
||||
AvoidWeekends bool `json:"avoid_weekends"`
|
||||
MinHoldTime int `json:"min_hold_time"` // Minimum hold time (minutes)
|
||||
MaxHoldTime int `json:"max_hold_time"` // Maximum hold time (minutes)
|
||||
ScanInterval int `json:"scan_interval"` // How often to scan (minutes)
|
||||
}
|
||||
|
||||
// TimeRange represents a time range
|
||||
type TimeRange struct {
|
||||
Start string `json:"start"` // "09:00"
|
||||
End string `json:"end"` // "17:00"
|
||||
TZ string `json:"tz"` // Timezone
|
||||
}
|
||||
|
||||
// AIStrategyConfig defines AI-specific settings
|
||||
type AIStrategyConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Model string `json:"model"` // AI model to use
|
||||
ConfidenceThreshold float64 `json:"confidence_threshold"` // Min confidence to act
|
||||
UseMarketSentiment bool `json:"use_market_sentiment"`
|
||||
UseTechnicalAnalysis bool `json:"use_technical_analysis"`
|
||||
UseOnChainData bool `json:"use_onchain_data"`
|
||||
CustomPrompt string `json:"custom_prompt"` // Custom instructions for AI
|
||||
Personality string `json:"personality"` // "aggressive", "conservative", "balanced"
|
||||
}
|
||||
|
||||
// StrategyPerformance tracks strategy performance
|
||||
type StrategyPerformance struct {
|
||||
TotalTrades int `json:"total_trades"`
|
||||
WinningTrades int `json:"winning_trades"`
|
||||
LosingTrades int `json:"losing_trades"`
|
||||
WinRate float64 `json:"win_rate"`
|
||||
TotalPnL float64 `json:"total_pnl"`
|
||||
MaxDrawdown float64 `json:"max_drawdown"`
|
||||
SharpeRatio float64 `json:"sharpe_ratio"`
|
||||
ProfitFactor float64 `json:"profit_factor"`
|
||||
AvgWin float64 `json:"avg_win"`
|
||||
AvgLoss float64 `json:"avg_loss"`
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
}
|
||||
|
||||
// StrategyBuilder helps users create strategies through conversation
|
||||
type StrategyBuilder struct {
|
||||
store *store.Store
|
||||
}
|
||||
|
||||
// NewStrategyBuilder creates a new strategy builder
|
||||
func NewStrategyBuilder(st *store.Store) *StrategyBuilder {
|
||||
return &StrategyBuilder{store: st}
|
||||
}
|
||||
|
||||
// CreateFromNaturalLanguage creates a strategy from natural language description
|
||||
func (sb *StrategyBuilder) CreateFromNaturalLanguage(description string, userID string) (*SmartStrategy, error) {
|
||||
// This would typically call an AI to parse the description
|
||||
// For now, we create a basic template
|
||||
strategy := &SmartStrategy{
|
||||
ID: uuid.New().String()[:8],
|
||||
Name: "Custom Strategy",
|
||||
Description: description,
|
||||
Type: StrategyTypeAI,
|
||||
SymbolMode: "ai_select",
|
||||
MaxSymbols: 5,
|
||||
EntryMode: "all",
|
||||
MaxPositions: 5,
|
||||
MaxPerSymbol: 1,
|
||||
PositionSize: PositionSizeConfig{
|
||||
Mode: "percent",
|
||||
PercentOfEquity: 5,
|
||||
MaxSingleTrade: 1000,
|
||||
},
|
||||
RiskConfig: RiskConfig{
|
||||
MaxDrawdown: 20,
|
||||
MaxDailyLoss: 5,
|
||||
MaxOpenRisk: 10,
|
||||
CooldownAfterLoss: 30,
|
||||
RequireConfirmation: true,
|
||||
EmergencyStopLoss: 30,
|
||||
},
|
||||
LeverageConfig: LeverageConfig{
|
||||
Mode: "dynamic",
|
||||
DefaultLeverage: 3,
|
||||
MaxLeverage: 10,
|
||||
},
|
||||
TimeConfig: TimeConfig{
|
||||
ScanInterval: 5,
|
||||
AvoidWeekends: false,
|
||||
},
|
||||
AIConfig: AIStrategyConfig{
|
||||
Enabled: true,
|
||||
ConfidenceThreshold: 0.7,
|
||||
UseMarketSentiment: true,
|
||||
UseTechnicalAnalysis: true,
|
||||
Personality: "balanced",
|
||||
CustomPrompt: description,
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
CreatedBy: userID,
|
||||
IsActive: false,
|
||||
}
|
||||
|
||||
return strategy, nil
|
||||
}
|
||||
|
||||
// CreateGridStrategy creates a grid trading strategy
|
||||
func (sb *StrategyBuilder) CreateGridStrategy(symbol string, lowerPrice, upperPrice float64, gridCount int, amountPerGrid float64) *SmartStrategy {
|
||||
return &SmartStrategy{
|
||||
ID: uuid.New().String()[:8],
|
||||
Name: fmt.Sprintf("Grid %s", symbol),
|
||||
Description: fmt.Sprintf("Grid trading %s from %.2f to %.2f with %d grids", symbol, lowerPrice, upperPrice, gridCount),
|
||||
Type: StrategyTypeGrid,
|
||||
Symbols: []string{symbol},
|
||||
SymbolMode: "static",
|
||||
MaxPositions: gridCount,
|
||||
PositionSize: PositionSizeConfig{
|
||||
Mode: "fixed",
|
||||
FixedAmount: amountPerGrid,
|
||||
},
|
||||
EntryRules: []Rule{
|
||||
{
|
||||
ID: "grid_entry",
|
||||
Type: "price",
|
||||
Condition: "grid_level",
|
||||
Value: map[string]interface{}{
|
||||
"lower_price": lowerPrice,
|
||||
"upper_price": upperPrice,
|
||||
"grid_count": gridCount,
|
||||
},
|
||||
},
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
IsActive: false,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDCAStrategy creates a DCA strategy
|
||||
func (sb *StrategyBuilder) CreateDCAStrategy(symbol string, intervalMinutes int, amountPerBuy float64, maxBuys int) *SmartStrategy {
|
||||
return &SmartStrategy{
|
||||
ID: uuid.New().String()[:8],
|
||||
Name: fmt.Sprintf("DCA %s", symbol),
|
||||
Description: fmt.Sprintf("DCA into %s every %d minutes, $%.2f per buy, max %d buys", symbol, intervalMinutes, amountPerBuy, maxBuys),
|
||||
Type: StrategyTypeDCA,
|
||||
Symbols: []string{symbol},
|
||||
SymbolMode: "static",
|
||||
MaxPositions: maxBuys,
|
||||
PositionSize: PositionSizeConfig{
|
||||
Mode: "fixed",
|
||||
FixedAmount: amountPerBuy,
|
||||
},
|
||||
TimeConfig: TimeConfig{
|
||||
ScanInterval: intervalMinutes,
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
IsActive: false,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTrendStrategy creates a trend following strategy
|
||||
func (sb *StrategyBuilder) CreateTrendStrategy(symbols []string, emaFast, emaSlow int, leverage int) *SmartStrategy {
|
||||
return &SmartStrategy{
|
||||
ID: uuid.New().String()[:8],
|
||||
Name: "Trend Following",
|
||||
Description: fmt.Sprintf("EMA %d/%d crossover strategy", emaFast, emaSlow),
|
||||
Type: StrategyTypeTrend,
|
||||
Symbols: symbols,
|
||||
SymbolMode: "static",
|
||||
EntryMode: "all",
|
||||
EntryRules: []Rule{
|
||||
{
|
||||
ID: "ema_cross",
|
||||
Name: "EMA Crossover",
|
||||
Type: "indicator",
|
||||
Indicator: "EMA",
|
||||
Condition: "crosses_above",
|
||||
Value: map[string]int{
|
||||
"fast_period": emaFast,
|
||||
"slow_period": emaSlow,
|
||||
},
|
||||
Timeframe: "1h",
|
||||
Weight: 1.0,
|
||||
},
|
||||
},
|
||||
ExitRules: []Rule{
|
||||
{
|
||||
ID: "ema_cross_exit",
|
||||
Name: "EMA Crossover Exit",
|
||||
Type: "indicator",
|
||||
Indicator: "EMA",
|
||||
Condition: "crosses_below",
|
||||
Value: map[string]int{
|
||||
"fast_period": emaFast,
|
||||
"slow_period": emaSlow,
|
||||
},
|
||||
Timeframe: "1h",
|
||||
},
|
||||
},
|
||||
LeverageConfig: LeverageConfig{
|
||||
Mode: "fixed",
|
||||
DefaultLeverage: leverage,
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
IsActive: false,
|
||||
}
|
||||
}
|
||||
|
||||
// StrategyToPrompt converts a strategy to an AI prompt
|
||||
func StrategyToPrompt(s *SmartStrategy) string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("# 策略: %s\n\n", s.Name))
|
||||
sb.WriteString(fmt.Sprintf("**描述**: %s\n", s.Description))
|
||||
sb.WriteString(fmt.Sprintf("**类型**: %s\n\n", s.Type))
|
||||
|
||||
// Trading pairs
|
||||
if len(s.Symbols) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("**交易对**: %s\n", strings.Join(s.Symbols, ", ")))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("**选币模式**: %s (最多 %d 个)\n", s.SymbolMode, s.MaxSymbols))
|
||||
}
|
||||
|
||||
// Entry rules
|
||||
if len(s.EntryRules) > 0 {
|
||||
sb.WriteString("\n## 入场规则\n")
|
||||
for _, rule := range s.EntryRules {
|
||||
sb.WriteString(fmt.Sprintf("- %s: %s %s %v\n", rule.Name, rule.Indicator, rule.Condition, rule.Value))
|
||||
}
|
||||
}
|
||||
|
||||
// Exit rules
|
||||
sb.WriteString("\n## 出场规则\n")
|
||||
if s.TakeProfit != nil {
|
||||
sb.WriteString(fmt.Sprintf("- 止盈: %.1f%%\n", *s.TakeProfit))
|
||||
}
|
||||
if s.StopLoss != nil {
|
||||
sb.WriteString(fmt.Sprintf("- 止损: %.1f%%\n", *s.StopLoss))
|
||||
}
|
||||
if s.TrailingStop != nil {
|
||||
sb.WriteString(fmt.Sprintf("- 移动止损: %.1f%%\n", *s.TrailingStop))
|
||||
}
|
||||
|
||||
// Risk management
|
||||
sb.WriteString("\n## 风险管理\n")
|
||||
sb.WriteString(fmt.Sprintf("- 最大回撤: %.1f%%\n", s.RiskConfig.MaxDrawdown))
|
||||
sb.WriteString(fmt.Sprintf("- 单日最大亏损: %.1f%%\n", s.RiskConfig.MaxDailyLoss))
|
||||
sb.WriteString(fmt.Sprintf("- 最大持仓数: %d\n", s.MaxPositions))
|
||||
|
||||
// AI settings
|
||||
if s.AIConfig.Enabled {
|
||||
sb.WriteString("\n## AI 配置\n")
|
||||
sb.WriteString(fmt.Sprintf("- 置信度阈值: %.0f%%\n", s.AIConfig.ConfidenceThreshold*100))
|
||||
sb.WriteString(fmt.Sprintf("- 风格: %s\n", s.AIConfig.Personality))
|
||||
if s.AIConfig.CustomPrompt != "" {
|
||||
sb.WriteString(fmt.Sprintf("- 自定义指令: %s\n", s.AIConfig.CustomPrompt))
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
548
assistant/strategy_tools.go
Normal file
548
assistant/strategy_tools.go
Normal file
@@ -0,0 +1,548 @@
|
||||
package assistant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"nofx/store"
|
||||
)
|
||||
|
||||
// StrategyTools provides strategy management tools for the AI agent
|
||||
type StrategyTools struct {
|
||||
store *store.Store
|
||||
strategyBuilder *StrategyBuilder
|
||||
strategies map[string]*SmartStrategy // In-memory strategy cache
|
||||
}
|
||||
|
||||
// NewStrategyTools creates strategy tools
|
||||
func NewStrategyTools(st *store.Store) *StrategyTools {
|
||||
return &StrategyTools{
|
||||
store: st,
|
||||
strategyBuilder: NewStrategyBuilder(st),
|
||||
strategies: make(map[string]*SmartStrategy),
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllTools returns all strategy tools
|
||||
func (st *StrategyTools) GetAllTools() []Tool {
|
||||
return []Tool{
|
||||
st.CreateStrategyTool(),
|
||||
st.CreateGridStrategyTool(),
|
||||
st.CreateDCAStrategyTool(),
|
||||
st.CreateTrendStrategyTool(),
|
||||
st.ListSmartStrategiesTool(),
|
||||
st.GetStrategyDetailsTool(),
|
||||
st.UpdateStrategyTool(),
|
||||
st.ActivateStrategyTool(),
|
||||
st.DeactivateStrategyTool(),
|
||||
st.DeleteStrategyTool(),
|
||||
st.GetStrategyTemplates(),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateStrategyTool creates a strategy from natural language
|
||||
func (st *StrategyTools) CreateStrategyTool() Tool {
|
||||
return NewTool(
|
||||
"create_strategy",
|
||||
`Create a new trading strategy from natural language description.
|
||||
Examples:
|
||||
- "当RSI低于30时买入BTC,RSI高于70时卖出"
|
||||
- "每天定投100美元ETH"
|
||||
- "BTC在5万到6万之间做网格交易"`,
|
||||
`{
|
||||
"name": "string (required) - Strategy name",
|
||||
"description": "string (required) - Natural language description of the strategy",
|
||||
"symbols": "array (optional) - Trading pairs, e.g., [\"BTCUSDT\", \"ETHUSDT\"]",
|
||||
"take_profit": "number (optional) - Take profit percentage",
|
||||
"stop_loss": "number (optional) - Stop loss percentage",
|
||||
"leverage": "number (optional) - Leverage to use (default: 3)",
|
||||
"max_positions": "number (optional) - Max concurrent positions (default: 5)"
|
||||
}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
var params struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Symbols []string `json:"symbols"`
|
||||
TakeProfit *float64 `json:"take_profit"`
|
||||
StopLoss *float64 `json:"stop_loss"`
|
||||
Leverage int `json:"leverage"`
|
||||
MaxPositions int `json:"max_positions"`
|
||||
}
|
||||
if err := json.Unmarshal(args, ¶ms); err != nil {
|
||||
return nil, fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
|
||||
if params.Description == "" {
|
||||
return nil, fmt.Errorf("strategy description is required")
|
||||
}
|
||||
|
||||
strategy, err := st.strategyBuilder.CreateFromNaturalLanguage(params.Description, "default")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Apply user customizations
|
||||
if params.Name != "" {
|
||||
strategy.Name = params.Name
|
||||
}
|
||||
if len(params.Symbols) > 0 {
|
||||
strategy.Symbols = params.Symbols
|
||||
strategy.SymbolMode = "static"
|
||||
}
|
||||
if params.TakeProfit != nil {
|
||||
strategy.TakeProfit = params.TakeProfit
|
||||
}
|
||||
if params.StopLoss != nil {
|
||||
strategy.StopLoss = params.StopLoss
|
||||
}
|
||||
if params.Leverage > 0 {
|
||||
strategy.LeverageConfig.DefaultLeverage = params.Leverage
|
||||
}
|
||||
if params.MaxPositions > 0 {
|
||||
strategy.MaxPositions = params.MaxPositions
|
||||
}
|
||||
|
||||
// Store in memory
|
||||
st.strategies[strategy.ID] = strategy
|
||||
|
||||
return map[string]interface{}{
|
||||
"success": true,
|
||||
"strategy": strategy,
|
||||
"message": fmt.Sprintf("策略 '%s' (ID: %s) 创建成功!使用 activate_strategy 激活它。", strategy.Name, strategy.ID),
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// CreateGridStrategyTool creates a grid trading strategy
|
||||
func (st *StrategyTools) CreateGridStrategyTool() Tool {
|
||||
return NewTool(
|
||||
"create_grid_strategy",
|
||||
"Create a grid trading strategy. Grid trading places buy and sell orders at predetermined price levels.",
|
||||
`{
|
||||
"symbol": "string (required) - Trading pair, e.g., BTCUSDT",
|
||||
"lower_price": "number (required) - Lower price bound",
|
||||
"upper_price": "number (required) - Upper price bound",
|
||||
"grid_count": "number (required) - Number of grids (10-100)",
|
||||
"amount_per_grid": "number (required) - USDT amount per grid"
|
||||
}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
var params struct {
|
||||
Symbol string `json:"symbol"`
|
||||
LowerPrice float64 `json:"lower_price"`
|
||||
UpperPrice float64 `json:"upper_price"`
|
||||
GridCount int `json:"grid_count"`
|
||||
AmountPerGrid float64 `json:"amount_per_grid"`
|
||||
}
|
||||
if err := json.Unmarshal(args, ¶ms); err != nil {
|
||||
return nil, fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
|
||||
if params.LowerPrice >= params.UpperPrice {
|
||||
return nil, fmt.Errorf("lower_price must be less than upper_price")
|
||||
}
|
||||
if params.GridCount < 2 || params.GridCount > 100 {
|
||||
return nil, fmt.Errorf("grid_count must be between 2 and 100")
|
||||
}
|
||||
|
||||
strategy := st.strategyBuilder.CreateGridStrategy(
|
||||
params.Symbol, params.LowerPrice, params.UpperPrice,
|
||||
params.GridCount, params.AmountPerGrid,
|
||||
)
|
||||
st.strategies[strategy.ID] = strategy
|
||||
|
||||
gridSize := (params.UpperPrice - params.LowerPrice) / float64(params.GridCount)
|
||||
totalInvestment := params.AmountPerGrid * float64(params.GridCount)
|
||||
|
||||
return map[string]interface{}{
|
||||
"success": true,
|
||||
"strategy": strategy,
|
||||
"details": map[string]interface{}{
|
||||
"grid_size": gridSize,
|
||||
"total_investment": totalInvestment,
|
||||
"profit_per_grid": (gridSize / params.LowerPrice) * 100,
|
||||
},
|
||||
"message": fmt.Sprintf("网格策略创建成功!\n价格区间: %.2f - %.2f\n网格数: %d\n每格间距: %.2f\n总投资: $%.2f",
|
||||
params.LowerPrice, params.UpperPrice, params.GridCount, gridSize, totalInvestment),
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// CreateDCAStrategyTool creates a DCA strategy
|
||||
func (st *StrategyTools) CreateDCAStrategyTool() Tool {
|
||||
return NewTool(
|
||||
"create_dca_strategy",
|
||||
"Create a Dollar Cost Averaging (DCA) strategy. Automatically buy at regular intervals.",
|
||||
`{
|
||||
"symbol": "string (required) - Trading pair, e.g., BTCUSDT",
|
||||
"interval_minutes": "number (required) - Buy interval in minutes (min: 5)",
|
||||
"amount_per_buy": "number (required) - USDT amount per purchase",
|
||||
"max_buys": "number (optional) - Maximum number of buys (default: unlimited)"
|
||||
}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
var params struct {
|
||||
Symbol string `json:"symbol"`
|
||||
IntervalMinutes int `json:"interval_minutes"`
|
||||
AmountPerBuy float64 `json:"amount_per_buy"`
|
||||
MaxBuys int `json:"max_buys"`
|
||||
}
|
||||
if err := json.Unmarshal(args, ¶ms); err != nil {
|
||||
return nil, fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
|
||||
if params.IntervalMinutes < 5 {
|
||||
return nil, fmt.Errorf("interval must be at least 5 minutes")
|
||||
}
|
||||
if params.MaxBuys == 0 {
|
||||
params.MaxBuys = 1000 // Effectively unlimited
|
||||
}
|
||||
|
||||
strategy := st.strategyBuilder.CreateDCAStrategy(
|
||||
params.Symbol, params.IntervalMinutes, params.AmountPerBuy, params.MaxBuys,
|
||||
)
|
||||
st.strategies[strategy.ID] = strategy
|
||||
|
||||
return map[string]interface{}{
|
||||
"success": true,
|
||||
"strategy": strategy,
|
||||
"message": fmt.Sprintf("DCA策略创建成功!\n币种: %s\n定投间隔: %d分钟\n每次金额: $%.2f\n最大次数: %d",
|
||||
params.Symbol, params.IntervalMinutes, params.AmountPerBuy, params.MaxBuys),
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// CreateTrendStrategyTool creates a trend following strategy
|
||||
func (st *StrategyTools) CreateTrendStrategyTool() Tool {
|
||||
return NewTool(
|
||||
"create_trend_strategy",
|
||||
"Create a trend following strategy using EMA crossover.",
|
||||
`{
|
||||
"symbols": "array (required) - Trading pairs",
|
||||
"ema_fast": "number (optional) - Fast EMA period (default: 9)",
|
||||
"ema_slow": "number (optional) - Slow EMA period (default: 21)",
|
||||
"leverage": "number (optional) - Leverage (default: 3)",
|
||||
"take_profit": "number (optional) - Take profit %",
|
||||
"stop_loss": "number (optional) - Stop loss %"
|
||||
}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
var params struct {
|
||||
Symbols []string `json:"symbols"`
|
||||
EMAFast int `json:"ema_fast"`
|
||||
EMASlow int `json:"ema_slow"`
|
||||
Leverage int `json:"leverage"`
|
||||
TakeProfit *float64 `json:"take_profit"`
|
||||
StopLoss *float64 `json:"stop_loss"`
|
||||
}
|
||||
if err := json.Unmarshal(args, ¶ms); err != nil {
|
||||
return nil, fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
|
||||
if len(params.Symbols) == 0 {
|
||||
params.Symbols = []string{"BTCUSDT", "ETHUSDT"}
|
||||
}
|
||||
if params.EMAFast == 0 {
|
||||
params.EMAFast = 9
|
||||
}
|
||||
if params.EMASlow == 0 {
|
||||
params.EMASlow = 21
|
||||
}
|
||||
if params.Leverage == 0 {
|
||||
params.Leverage = 3
|
||||
}
|
||||
|
||||
strategy := st.strategyBuilder.CreateTrendStrategy(
|
||||
params.Symbols, params.EMAFast, params.EMASlow, params.Leverage,
|
||||
)
|
||||
strategy.TakeProfit = params.TakeProfit
|
||||
strategy.StopLoss = params.StopLoss
|
||||
st.strategies[strategy.ID] = strategy
|
||||
|
||||
return map[string]interface{}{
|
||||
"success": true,
|
||||
"strategy": strategy,
|
||||
"message": fmt.Sprintf("趋势策略创建成功!\nEMA %d/%d 交叉\n交易对: %v\n杠杆: %dx",
|
||||
params.EMAFast, params.EMASlow, params.Symbols, params.Leverage),
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// ListSmartStrategiesTool lists all smart strategies
|
||||
func (st *StrategyTools) ListSmartStrategiesTool() Tool {
|
||||
return NewTool(
|
||||
"list_smart_strategies",
|
||||
"List all smart strategies (both in-memory and saved).",
|
||||
`{}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
var result []map[string]interface{}
|
||||
|
||||
for _, s := range st.strategies {
|
||||
result = append(result, map[string]interface{}{
|
||||
"id": s.ID,
|
||||
"name": s.Name,
|
||||
"type": s.Type,
|
||||
"description": s.Description,
|
||||
"is_active": s.IsActive,
|
||||
"symbols": s.Symbols,
|
||||
"created_at": s.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// Also get strategies from store
|
||||
if dbStrategies, err := st.store.Strategy().List("default"); err == nil {
|
||||
for _, s := range dbStrategies {
|
||||
result = append(result, map[string]interface{}{
|
||||
"id": s.ID,
|
||||
"name": s.Name,
|
||||
"type": "db_strategy",
|
||||
"description": s.Description,
|
||||
"is_active": s.IsActive,
|
||||
"source": "database",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
return map[string]interface{}{
|
||||
"strategies": []interface{}{},
|
||||
"message": "暂无策略。使用 create_strategy 创建一个新策略。",
|
||||
}, nil
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"strategies": result,
|
||||
"count": len(result),
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// GetStrategyDetailsTool gets detailed strategy info
|
||||
func (st *StrategyTools) GetStrategyDetailsTool() Tool {
|
||||
return NewTool(
|
||||
"get_strategy_details",
|
||||
"Get detailed information about a specific strategy.",
|
||||
`{"strategy_id": "string (required)"}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
var params struct {
|
||||
StrategyID string `json:"strategy_id"`
|
||||
}
|
||||
if err := json.Unmarshal(args, ¶ms); err != nil {
|
||||
return nil, fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
|
||||
if s, ok := st.strategies[params.StrategyID]; ok {
|
||||
return map[string]interface{}{
|
||||
"strategy": s,
|
||||
"prompt_text": StrategyToPrompt(s),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("strategy not found: %s", params.StrategyID)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateStrategyTool updates a strategy
|
||||
func (st *StrategyTools) UpdateStrategyTool() Tool {
|
||||
return NewTool(
|
||||
"update_strategy",
|
||||
"Update an existing strategy's settings.",
|
||||
`{
|
||||
"strategy_id": "string (required)",
|
||||
"name": "string (optional)",
|
||||
"take_profit": "number (optional)",
|
||||
"stop_loss": "number (optional)",
|
||||
"leverage": "number (optional)",
|
||||
"max_positions": "number (optional)",
|
||||
"symbols": "array (optional)"
|
||||
}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
var params struct {
|
||||
StrategyID string `json:"strategy_id"`
|
||||
Name string `json:"name"`
|
||||
TakeProfit *float64 `json:"take_profit"`
|
||||
StopLoss *float64 `json:"stop_loss"`
|
||||
Leverage int `json:"leverage"`
|
||||
MaxPositions int `json:"max_positions"`
|
||||
Symbols []string `json:"symbols"`
|
||||
}
|
||||
if err := json.Unmarshal(args, ¶ms); err != nil {
|
||||
return nil, fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
|
||||
s, ok := st.strategies[params.StrategyID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("strategy not found: %s", params.StrategyID)
|
||||
}
|
||||
|
||||
if params.Name != "" {
|
||||
s.Name = params.Name
|
||||
}
|
||||
if params.TakeProfit != nil {
|
||||
s.TakeProfit = params.TakeProfit
|
||||
}
|
||||
if params.StopLoss != nil {
|
||||
s.StopLoss = params.StopLoss
|
||||
}
|
||||
if params.Leverage > 0 {
|
||||
s.LeverageConfig.DefaultLeverage = params.Leverage
|
||||
}
|
||||
if params.MaxPositions > 0 {
|
||||
s.MaxPositions = params.MaxPositions
|
||||
}
|
||||
if len(params.Symbols) > 0 {
|
||||
s.Symbols = params.Symbols
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"success": true,
|
||||
"strategy": s,
|
||||
"message": "策略已更新",
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// ActivateStrategyTool activates a strategy
|
||||
func (st *StrategyTools) ActivateStrategyTool() Tool {
|
||||
return NewTool(
|
||||
"activate_strategy",
|
||||
"Activate a strategy to start trading. ⚠️ This will start real trading!",
|
||||
`{"strategy_id": "string (required)"}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
var params struct {
|
||||
StrategyID string `json:"strategy_id"`
|
||||
}
|
||||
if err := json.Unmarshal(args, ¶ms); err != nil {
|
||||
return nil, fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
|
||||
s, ok := st.strategies[params.StrategyID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("strategy not found: %s", params.StrategyID)
|
||||
}
|
||||
|
||||
s.IsActive = true
|
||||
|
||||
return map[string]interface{}{
|
||||
"success": true,
|
||||
"message": fmt.Sprintf("⚠️ 策略 '%s' 已激活!将开始真实交易。", s.Name),
|
||||
"strategy": s,
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// DeactivateStrategyTool deactivates a strategy
|
||||
func (st *StrategyTools) DeactivateStrategyTool() Tool {
|
||||
return NewTool(
|
||||
"deactivate_strategy",
|
||||
"Deactivate a strategy to stop trading.",
|
||||
`{"strategy_id": "string (required)"}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
var params struct {
|
||||
StrategyID string `json:"strategy_id"`
|
||||
}
|
||||
if err := json.Unmarshal(args, ¶ms); err != nil {
|
||||
return nil, fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
|
||||
s, ok := st.strategies[params.StrategyID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("strategy not found: %s", params.StrategyID)
|
||||
}
|
||||
|
||||
s.IsActive = false
|
||||
|
||||
return map[string]interface{}{
|
||||
"success": true,
|
||||
"message": fmt.Sprintf("策略 '%s' 已停用", s.Name),
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// DeleteStrategyTool deletes a strategy
|
||||
func (st *StrategyTools) DeleteStrategyTool() Tool {
|
||||
return NewTool(
|
||||
"delete_strategy",
|
||||
"Delete a strategy permanently.",
|
||||
`{"strategy_id": "string (required)"}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
var params struct {
|
||||
StrategyID string `json:"strategy_id"`
|
||||
}
|
||||
if err := json.Unmarshal(args, ¶ms); err != nil {
|
||||
return nil, fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
|
||||
if _, ok := st.strategies[params.StrategyID]; !ok {
|
||||
return nil, fmt.Errorf("strategy not found: %s", params.StrategyID)
|
||||
}
|
||||
|
||||
delete(st.strategies, params.StrategyID)
|
||||
|
||||
return map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "策略已删除",
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// GetStrategyTemplates returns available strategy templates
|
||||
func (st *StrategyTools) GetStrategyTemplates() Tool {
|
||||
return NewTool(
|
||||
"get_strategy_templates",
|
||||
"Get available strategy templates and examples.",
|
||||
`{}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
templates := []map[string]interface{}{
|
||||
{
|
||||
"name": "AI 智能交易",
|
||||
"type": "ai",
|
||||
"description": "让 AI 自主分析市场并决策,适合不想手动盯盘的用户",
|
||||
"example": "create_strategy(name='AI智能', description='分析BTC和ETH的技术指标和市场情绪,在有明确趋势时入场')",
|
||||
},
|
||||
{
|
||||
"name": "网格交易",
|
||||
"type": "grid",
|
||||
"description": "在价格区间内自动低买高卖,适合震荡行情",
|
||||
"example": "create_grid_strategy(symbol='BTCUSDT', lower_price=90000, upper_price=100000, grid_count=20, amount_per_grid=100)",
|
||||
},
|
||||
{
|
||||
"name": "定投 DCA",
|
||||
"type": "dca",
|
||||
"description": "定期定额买入,摊薄成本,适合长期投资",
|
||||
"example": "create_dca_strategy(symbol='ETHUSDT', interval_minutes=1440, amount_per_buy=50, max_buys=365)",
|
||||
},
|
||||
{
|
||||
"name": "趋势跟踪",
|
||||
"type": "trend",
|
||||
"description": "跟随趋势,EMA金叉买入死叉卖出",
|
||||
"example": "create_trend_strategy(symbols=['BTCUSDT','ETHUSDT'], ema_fast=9, ema_slow=21, leverage=3)",
|
||||
},
|
||||
{
|
||||
"name": "RSI 超买超卖",
|
||||
"type": "custom",
|
||||
"description": "RSI 低于 30 买入,高于 70 卖出",
|
||||
"example": "create_strategy(name='RSI策略', description='当RSI14低于30时买入,高于70时卖出,止损10%')",
|
||||
},
|
||||
{
|
||||
"name": "突破策略",
|
||||
"type": "breakout",
|
||||
"description": "价格突破关键位时入场",
|
||||
"example": "create_strategy(name='突破策略', description='当价格突破20日最高点时做多,突破20日最低点时做空')",
|
||||
},
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"templates": templates,
|
||||
"message": "以上是可用的策略模板,选择一个并告诉我你想怎么定制!",
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
47
assistant/tool.go
Normal file
47
assistant/tool.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package assistant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Tool represents a callable tool that the AI agent can use
|
||||
type Tool interface {
|
||||
// Name returns the tool's unique identifier
|
||||
Name() string
|
||||
|
||||
// Description returns a human-readable description for the AI
|
||||
Description() string
|
||||
|
||||
// ParameterSchema returns JSON schema for the tool's parameters
|
||||
ParameterSchema() string
|
||||
|
||||
// Execute runs the tool with the given arguments
|
||||
Execute(ctx context.Context, args json.RawMessage) (interface{}, error)
|
||||
}
|
||||
|
||||
// BaseTool provides common functionality for tools
|
||||
type BaseTool struct {
|
||||
ToolName string
|
||||
ToolDescription string
|
||||
ToolSchema string
|
||||
ExecuteFunc func(ctx context.Context, args json.RawMessage) (interface{}, error)
|
||||
}
|
||||
|
||||
func (t *BaseTool) Name() string { return t.ToolName }
|
||||
func (t *BaseTool) Description() string { return t.ToolDescription }
|
||||
func (t *BaseTool) ParameterSchema() string { return t.ToolSchema }
|
||||
|
||||
func (t *BaseTool) Execute(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
return t.ExecuteFunc(ctx, args)
|
||||
}
|
||||
|
||||
// NewTool creates a simple tool from a function
|
||||
func NewTool(name, description, schema string, fn func(ctx context.Context, args json.RawMessage) (interface{}, error)) Tool {
|
||||
return &BaseTool{
|
||||
ToolName: name,
|
||||
ToolDescription: description,
|
||||
ToolSchema: schema,
|
||||
ExecuteFunc: fn,
|
||||
}
|
||||
}
|
||||
530
assistant/trading_tools.go
Normal file
530
assistant/trading_tools.go
Normal file
@@ -0,0 +1,530 @@
|
||||
package assistant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"nofx/logger"
|
||||
"nofx/manager"
|
||||
"nofx/store"
|
||||
)
|
||||
|
||||
// TradingTools provides all trading-related tools for the AI agent
|
||||
type TradingTools struct {
|
||||
traderManager *manager.TraderManager
|
||||
store *store.Store
|
||||
}
|
||||
|
||||
// NewTradingTools creates trading tools with access to NOFX core
|
||||
func NewTradingTools(tm *manager.TraderManager, st *store.Store) *TradingTools {
|
||||
return &TradingTools{
|
||||
traderManager: tm,
|
||||
store: st,
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllTools returns all trading tools
|
||||
func (t *TradingTools) GetAllTools() []Tool {
|
||||
return []Tool{
|
||||
t.GetBalanceTool(),
|
||||
t.GetPositionsTool(),
|
||||
t.ListTradersTool(),
|
||||
t.GetTraderStatusTool(),
|
||||
t.StartTraderTool(),
|
||||
t.StopTraderTool(),
|
||||
t.GetMarketPriceTool(),
|
||||
t.OpenLongTool(),
|
||||
t.OpenShortTool(),
|
||||
t.ClosePositionTool(),
|
||||
t.ListStrategiesTool(),
|
||||
t.ListExchangesTool(),
|
||||
t.ListAIModelsTool(),
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Query Tools ====================
|
||||
|
||||
// GetBalanceTool returns the get_balance tool
|
||||
func (t *TradingTools) GetBalanceTool() Tool {
|
||||
return NewTool(
|
||||
"get_balance",
|
||||
"Get account balance for a trader. Returns available balance, total equity, and margin info.",
|
||||
`{"trader_id": "string (required) - The trader ID to query"}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
var params struct {
|
||||
TraderID string `json:"trader_id"`
|
||||
}
|
||||
if err := json.Unmarshal(args, ¶ms); err != nil {
|
||||
return nil, fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
|
||||
trader, err := t.traderManager.GetTrader(params.TraderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("trader not found: %w", err)
|
||||
}
|
||||
|
||||
balance, err := trader.GetAccountInfo()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get balance: %w", err)
|
||||
}
|
||||
|
||||
return balance, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// GetPositionsTool returns the get_positions tool
|
||||
func (t *TradingTools) GetPositionsTool() Tool {
|
||||
return NewTool(
|
||||
"get_positions",
|
||||
"Get all open positions for a trader. Returns symbol, side, size, entry price, unrealized P&L.",
|
||||
`{"trader_id": "string (required) - The trader ID to query"}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
var params struct {
|
||||
TraderID string `json:"trader_id"`
|
||||
}
|
||||
if err := json.Unmarshal(args, ¶ms); err != nil {
|
||||
return nil, fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
|
||||
trader, err := t.traderManager.GetTrader(params.TraderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("trader not found: %w", err)
|
||||
}
|
||||
|
||||
positions, err := trader.GetPositions()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get positions: %w", err)
|
||||
}
|
||||
|
||||
return positions, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// ListTradersTool returns the list_traders tool
|
||||
func (t *TradingTools) ListTradersTool() Tool {
|
||||
return NewTool(
|
||||
"list_traders",
|
||||
"List all configured AI traders with their status (running/stopped), exchange, AI model, and performance.",
|
||||
`{}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
traders, err := t.store.Trader().List("default")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list traders: %w", err)
|
||||
}
|
||||
|
||||
var result []map[string]interface{}
|
||||
for _, tr := range traders {
|
||||
traderInfo := map[string]interface{}{
|
||||
"id": tr.ID,
|
||||
"name": tr.Name,
|
||||
"is_running": tr.IsRunning,
|
||||
"ai_model_id": tr.AIModelID,
|
||||
"exchange_id": tr.ExchangeID,
|
||||
"strategy_id": tr.StrategyID,
|
||||
"created_at": tr.CreatedAt,
|
||||
}
|
||||
|
||||
// Try to get live status if trader is running
|
||||
if liveTrader, err := t.traderManager.GetTrader(tr.ID); err == nil {
|
||||
status := liveTrader.GetStatus()
|
||||
traderInfo["live_status"] = status
|
||||
}
|
||||
|
||||
result = append(result, traderInfo)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// GetTraderStatusTool returns detailed status of a specific trader
|
||||
func (t *TradingTools) GetTraderStatusTool() Tool {
|
||||
return NewTool(
|
||||
"get_trader_status",
|
||||
"Get detailed status of a specific trader including current positions, recent trades, and performance metrics.",
|
||||
`{"trader_id": "string (required) - The trader ID to query"}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
var params struct {
|
||||
TraderID string `json:"trader_id"`
|
||||
}
|
||||
if err := json.Unmarshal(args, ¶ms); err != nil {
|
||||
return nil, fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
|
||||
// Get trader config from store
|
||||
traderConfig, err := t.store.Trader().GetByID(params.TraderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("trader not found: %w", err)
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"id": traderConfig.ID,
|
||||
"name": traderConfig.Name,
|
||||
"is_running": traderConfig.IsRunning,
|
||||
"ai_model_id": traderConfig.AIModelID,
|
||||
"exchange_id": traderConfig.ExchangeID,
|
||||
"strategy_id": traderConfig.StrategyID,
|
||||
}
|
||||
|
||||
// If trader is running, get live data
|
||||
trader, err := t.traderManager.GetTrader(params.TraderID)
|
||||
if err == nil && trader != nil {
|
||||
result["live_status"] = trader.GetStatus()
|
||||
if balance, err := trader.GetAccountInfo(); err == nil {
|
||||
result["balance"] = balance
|
||||
}
|
||||
if positions, err := trader.GetPositions(); err == nil {
|
||||
result["positions"] = positions
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// ==================== Control Tools ====================
|
||||
|
||||
// StartTraderTool starts an AI trader
|
||||
func (t *TradingTools) StartTraderTool() Tool {
|
||||
return NewTool(
|
||||
"start_trader",
|
||||
"Start an AI trader to begin automated trading. The trader will execute trades based on its configured strategy.",
|
||||
`{"trader_id": "string (required) - The trader ID to start"}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
var params struct {
|
||||
TraderID string `json:"trader_id"`
|
||||
}
|
||||
if err := json.Unmarshal(args, ¶ms); err != nil {
|
||||
return nil, fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
|
||||
// Check if already running
|
||||
existingTrader, _ := t.traderManager.GetTrader(params.TraderID)
|
||||
if existingTrader != nil {
|
||||
status := existingTrader.GetStatus()
|
||||
if isRunning, ok := status["is_running"].(bool); ok && isRunning {
|
||||
return nil, fmt.Errorf("trader is already running")
|
||||
}
|
||||
// Remove from memory to reload
|
||||
t.traderManager.RemoveTrader(params.TraderID)
|
||||
}
|
||||
|
||||
// Load and start trader
|
||||
if err := t.traderManager.LoadUserTradersFromStore(t.store, "default"); err != nil {
|
||||
return nil, fmt.Errorf("failed to load trader: %w", err)
|
||||
}
|
||||
|
||||
trader, err := t.traderManager.GetTrader(params.TraderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get trader after load: %w", err)
|
||||
}
|
||||
|
||||
// Start the trader in a goroutine
|
||||
go func() {
|
||||
if err := trader.Run(); err != nil {
|
||||
logger.Errorf("Trader %s error: %v", params.TraderID, err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Update status in database
|
||||
if err := t.store.Trader().UpdateStatus("default", params.TraderID, true); err != nil {
|
||||
logger.Warnf("Failed to update trader status in DB: %v", err)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"success": true,
|
||||
"trader_id": params.TraderID,
|
||||
"message": "Trader started successfully",
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// StopTraderTool stops an AI trader
|
||||
func (t *TradingTools) StopTraderTool() Tool {
|
||||
return NewTool(
|
||||
"stop_trader",
|
||||
"Stop an AI trader. This will halt automated trading but keep existing positions open.",
|
||||
`{"trader_id": "string (required) - The trader ID to stop"}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
var params struct {
|
||||
TraderID string `json:"trader_id"`
|
||||
}
|
||||
if err := json.Unmarshal(args, ¶ms); err != nil {
|
||||
return nil, fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
|
||||
trader, err := t.traderManager.GetTrader(params.TraderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("trader not found: %w", err)
|
||||
}
|
||||
|
||||
// Check if running
|
||||
status := trader.GetStatus()
|
||||
if isRunning, ok := status["is_running"].(bool); ok && !isRunning {
|
||||
return nil, fmt.Errorf("trader is already stopped")
|
||||
}
|
||||
|
||||
// Stop the trader
|
||||
trader.Stop()
|
||||
|
||||
// Update status in database
|
||||
if err := t.store.Trader().UpdateStatus("default", params.TraderID, false); err != nil {
|
||||
logger.Warnf("Failed to update trader status in DB: %v", err)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"success": true,
|
||||
"trader_id": params.TraderID,
|
||||
"message": "Trader stopped successfully",
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// ==================== Trading Tools ====================
|
||||
|
||||
// GetMarketPriceTool gets current market price
|
||||
func (t *TradingTools) GetMarketPriceTool() Tool {
|
||||
return NewTool(
|
||||
"get_market_price",
|
||||
"Get current market price for a trading pair from a specific trader's exchange.",
|
||||
`{"trader_id": "string (required)", "symbol": "string (required) - e.g., BTCUSDT"}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
var params struct {
|
||||
TraderID string `json:"trader_id"`
|
||||
Symbol string `json:"symbol"`
|
||||
}
|
||||
if err := json.Unmarshal(args, ¶ms); err != nil {
|
||||
return nil, fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
|
||||
autoTrader, err := t.traderManager.GetTrader(params.TraderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("trader not found: %w", err)
|
||||
}
|
||||
|
||||
// Get the underlying trader interface
|
||||
underlyingTrader := autoTrader.GetUnderlyingTrader()
|
||||
if underlyingTrader == nil {
|
||||
return nil, fmt.Errorf("underlying trader not available")
|
||||
}
|
||||
|
||||
price, err := underlyingTrader.GetMarketPrice(params.Symbol)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get price: %w", err)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"symbol": params.Symbol,
|
||||
"price": price,
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// OpenLongTool opens a long position
|
||||
func (t *TradingTools) OpenLongTool() Tool {
|
||||
return NewTool(
|
||||
"open_long",
|
||||
"Open a long (buy) position. WARNING: This will execute a real trade!",
|
||||
`{"trader_id": "string (required)", "symbol": "string (required)", "quantity": "number (required)", "leverage": "number (optional, default 1)"}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
var params struct {
|
||||
TraderID string `json:"trader_id"`
|
||||
Symbol string `json:"symbol"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Leverage int `json:"leverage"`
|
||||
}
|
||||
if err := json.Unmarshal(args, ¶ms); err != nil {
|
||||
return nil, fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
|
||||
if params.Leverage == 0 {
|
||||
params.Leverage = 1
|
||||
}
|
||||
|
||||
autoTrader, err := t.traderManager.GetTrader(params.TraderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("trader not found: %w", err)
|
||||
}
|
||||
|
||||
underlyingTrader := autoTrader.GetUnderlyingTrader()
|
||||
if underlyingTrader == nil {
|
||||
return nil, fmt.Errorf("underlying trader not available")
|
||||
}
|
||||
|
||||
result, err := underlyingTrader.OpenLong(params.Symbol, params.Quantity, params.Leverage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open long: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// OpenShortTool opens a short position
|
||||
func (t *TradingTools) OpenShortTool() Tool {
|
||||
return NewTool(
|
||||
"open_short",
|
||||
"Open a short (sell) position. WARNING: This will execute a real trade!",
|
||||
`{"trader_id": "string (required)", "symbol": "string (required)", "quantity": "number (required)", "leverage": "number (optional, default 1)"}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
var params struct {
|
||||
TraderID string `json:"trader_id"`
|
||||
Symbol string `json:"symbol"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Leverage int `json:"leverage"`
|
||||
}
|
||||
if err := json.Unmarshal(args, ¶ms); err != nil {
|
||||
return nil, fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
|
||||
if params.Leverage == 0 {
|
||||
params.Leverage = 1
|
||||
}
|
||||
|
||||
autoTrader, err := t.traderManager.GetTrader(params.TraderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("trader not found: %w", err)
|
||||
}
|
||||
|
||||
underlyingTrader := autoTrader.GetUnderlyingTrader()
|
||||
if underlyingTrader == nil {
|
||||
return nil, fmt.Errorf("underlying trader not available")
|
||||
}
|
||||
|
||||
result, err := underlyingTrader.OpenShort(params.Symbol, params.Quantity, params.Leverage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open short: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// ClosePositionTool closes a position
|
||||
func (t *TradingTools) ClosePositionTool() Tool {
|
||||
return NewTool(
|
||||
"close_position",
|
||||
"Close an existing position (long or short). WARNING: This will execute a real trade!",
|
||||
`{"trader_id": "string (required)", "symbol": "string (required)", "side": "string (required) - 'long' or 'short'", "quantity": "number (optional) - leave empty to close all"}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
var params struct {
|
||||
TraderID string `json:"trader_id"`
|
||||
Symbol string `json:"symbol"`
|
||||
Side string `json:"side"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
}
|
||||
if err := json.Unmarshal(args, ¶ms); err != nil {
|
||||
return nil, fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
|
||||
autoTrader, err := t.traderManager.GetTrader(params.TraderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("trader not found: %w", err)
|
||||
}
|
||||
|
||||
underlyingTrader := autoTrader.GetUnderlyingTrader()
|
||||
if underlyingTrader == nil {
|
||||
return nil, fmt.Errorf("underlying trader not available")
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
|
||||
if params.Side == "long" {
|
||||
result, err = underlyingTrader.CloseLong(params.Symbol, params.Quantity)
|
||||
} else if params.Side == "short" {
|
||||
result, err = underlyingTrader.CloseShort(params.Symbol, params.Quantity)
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid side: %s (must be 'long' or 'short')", params.Side)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to close position: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// ==================== Config Tools ====================
|
||||
|
||||
// ListStrategiesTool lists all strategies
|
||||
func (t *TradingTools) ListStrategiesTool() Tool {
|
||||
return NewTool(
|
||||
"list_strategies",
|
||||
"List all trading strategies configured in the system.",
|
||||
`{}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
strategies, err := t.store.Strategy().List("default")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list strategies: %w", err)
|
||||
}
|
||||
return strategies, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// ListExchangesTool lists all exchange configurations
|
||||
func (t *TradingTools) ListExchangesTool() Tool {
|
||||
return NewTool(
|
||||
"list_exchanges",
|
||||
"List all configured exchanges (without showing API keys).",
|
||||
`{}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
exchanges, err := t.store.Exchange().List("default")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list exchanges: %w", err)
|
||||
}
|
||||
|
||||
// Remove sensitive data
|
||||
var result []map[string]interface{}
|
||||
for _, ex := range exchanges {
|
||||
result = append(result, map[string]interface{}{
|
||||
"id": ex.ID,
|
||||
"name": ex.Name,
|
||||
"exchange_type": ex.ExchangeType,
|
||||
"type": ex.Type,
|
||||
"enabled": ex.Enabled,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// ListAIModelsTool lists all AI model configurations
|
||||
func (t *TradingTools) ListAIModelsTool() Tool {
|
||||
return NewTool(
|
||||
"list_ai_models",
|
||||
"List all configured AI models (without showing API keys).",
|
||||
`{}`,
|
||||
func(ctx context.Context, args json.RawMessage) (interface{}, error) {
|
||||
models, err := t.store.AIModel().List("default")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list AI models: %w", err)
|
||||
}
|
||||
|
||||
// Remove sensitive data
|
||||
var result []map[string]interface{}
|
||||
for _, m := range models {
|
||||
result = append(result, map[string]interface{}{
|
||||
"id": m.ID,
|
||||
"name": m.Name,
|
||||
"provider": m.Provider,
|
||||
"custom_model": m.CustomModelName,
|
||||
"enabled": m.Enabled,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,26 +1,18 @@
|
||||
<h1 align="center">NOFX — オープンソース AI トレーディング OS</h1>
|
||||
# NOFX - AI トレーディングシステム
|
||||
|
||||
<p align="center">
|
||||
<strong>AI 駆動金融取引のインフラストラクチャレイヤー</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/NoFxAiOS/nofx/stargazers"><img src="https://img.shields.io/github/stars/NoFxAiOS/nofx?style=for-the-badge" alt="Stars"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/releases"><img src="https://img.shields.io/github/v/release/NoFxAiOS/nofx?style=for-the-badge" alt="Release"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-AGPL--3.0-blue.svg?style=for-the-badge" alt="License"></a>
|
||||
<a href="https://t.me/nofx_dev_community"><img src="https://img.shields.io/badge/Telegram-Community-blue?style=for-the-badge&logo=telegram" alt="Telegram"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://golang.org/"><img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go" alt="Go"></a>
|
||||
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
||||
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript" alt="TypeScript"></a>
|
||||
</p>
|
||||
[](https://golang.org/)
|
||||
[](https://reactjs.org/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](LICENSE)
|
||||
|
||||
**言語:** [English](../../../README.md) | [中文](../zh-CN/README.md) | [日本語](README.md)
|
||||
|
||||
---
|
||||
|
||||
## AI 駆動の暗号通貨取引プラットフォーム
|
||||
|
||||
**NOFX** は、複数の AI モデルを使用して暗号通貨先物を自動取引できるオープンソースの AI 取引システムです。Web インターフェースで戦略を設定し、リアルタイムでパフォーマンスを監視し、AI エージェントを競わせて最適な取引アプローチを見つけます。
|
||||
|
||||
### コア機能
|
||||
|
||||
- **マルチ AI サポート**: DeepSeek、Qwen、GPT、Claude、Gemini、Grok、Kimi を実行 - いつでもモデルを切り替え可能
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
<h1 align="center">NOFX — 오픈소스 AI 트레이딩 OS</h1>
|
||||
# NOFX - AI 트레이딩 시스템
|
||||
|
||||
<p align="center">
|
||||
<strong>AI 기반 금융 거래를 위한 인프라 레이어</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/NoFxAiOS/nofx/stargazers"><img src="https://img.shields.io/github/stars/NoFxAiOS/nofx?style=for-the-badge" alt="Stars"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/releases"><img src="https://img.shields.io/github/v/release/NoFxAiOS/nofx?style=for-the-badge" alt="Release"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-AGPL--3.0-blue.svg?style=for-the-badge" alt="License"></a>
|
||||
<a href="https://t.me/nofx_dev_community"><img src="https://img.shields.io/badge/Telegram-Community-blue?style=for-the-badge&logo=telegram" alt="Telegram"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://golang.org/"><img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go" alt="Go"></a>
|
||||
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
||||
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript" alt="TypeScript"></a>
|
||||
</p>
|
||||
[](https://golang.org/)
|
||||
[](https://reactjs.org/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](LICENSE)
|
||||
|
||||
**언어:** [English](../../../README.md) | [中文](../zh-CN/README.md) | [한국어](README.md)
|
||||
|
||||
---
|
||||
|
||||
## AI 기반 암호화폐 거래 플랫폼
|
||||
|
||||
**NOFX**는 여러 AI 모델을 실행하여 암호화폐 선물을 자동으로 거래할 수 있는 오픈소스 AI 거래 시스템입니다. 웹 인터페이스를 통해 전략을 구성하고, 실시간으로 성과를 모니터링하며, AI 에이전트들이 최적의 거래 방식을 찾도록 경쟁시킵니다.
|
||||
|
||||
### 핵심 기능
|
||||
|
||||
- **다중 AI 지원**: DeepSeek, Qwen, GPT, Claude, Gemini, Grok, Kimi 실행 - 언제든 모델 전환 가능
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
<h1 align="center">NOFX — Open Source AI Торговая ОС</h1>
|
||||
# NOFX - AI Торговая Система
|
||||
|
||||
<p align="center">
|
||||
<strong>Инфраструктурный слой для AI-powered финансовой торговли</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/NoFxAiOS/nofx/stargazers"><img src="https://img.shields.io/github/stars/NoFxAiOS/nofx?style=for-the-badge" alt="Stars"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/releases"><img src="https://img.shields.io/github/v/release/NoFxAiOS/nofx?style=for-the-badge" alt="Release"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-AGPL--3.0-blue.svg?style=for-the-badge" alt="License"></a>
|
||||
<a href="https://t.me/nofx_dev_community"><img src="https://img.shields.io/badge/Telegram-Community-blue?style=for-the-badge&logo=telegram" alt="Telegram"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://golang.org/"><img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go" alt="Go"></a>
|
||||
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
||||
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript" alt="TypeScript"></a>
|
||||
</p>
|
||||
[](https://golang.org/)
|
||||
[](https://reactjs.org/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](LICENSE)
|
||||
|
||||
**Языки:** [English](../../../README.md) | [中文](../zh-CN/README.md) | [Русский](README.md)
|
||||
|
||||
---
|
||||
|
||||
## Криптовалютная торговая платформа на базе ИИ
|
||||
|
||||
**NOFX** — это open-source AI торговая система, позволяющая запускать несколько AI моделей для автоматической торговли криптовалютными фьючерсами. Настраивайте стратегии через веб-интерфейс, отслеживайте эффективность в реальном времени и позвольте AI агентам конкурировать за лучший торговый подход.
|
||||
|
||||
### Основные функции
|
||||
|
||||
- **Мульти-AI поддержка**: Запускайте DeepSeek, Qwen, GPT, Claude, Gemini, Grok, Kimi — переключайтесь между моделями в любое время
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
<h1 align="center">NOFX — Open Source AI Торгова ОС</h1>
|
||||
# NOFX - AI Торгова Система
|
||||
|
||||
<p align="center">
|
||||
<strong>Інфраструктурний рівень для AI-powered фінансової торгівлі</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/NoFxAiOS/nofx/stargazers"><img src="https://img.shields.io/github/stars/NoFxAiOS/nofx?style=for-the-badge" alt="Stars"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/releases"><img src="https://img.shields.io/github/v/release/NoFxAiOS/nofx?style=for-the-badge" alt="Release"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-AGPL--3.0-blue.svg?style=for-the-badge" alt="License"></a>
|
||||
<a href="https://t.me/nofx_dev_community"><img src="https://img.shields.io/badge/Telegram-Community-blue?style=for-the-badge&logo=telegram" alt="Telegram"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://golang.org/"><img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go" alt="Go"></a>
|
||||
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
||||
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript" alt="TypeScript"></a>
|
||||
</p>
|
||||
[](https://golang.org/)
|
||||
[](https://reactjs.org/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](LICENSE)
|
||||
|
||||
**Мови:** [English](../../../README.md) | [中文](../zh-CN/README.md) | [Українська](README.md)
|
||||
|
||||
---
|
||||
|
||||
## Криптовалютна торгова платформа на базі ШІ
|
||||
|
||||
**NOFX** — це open-source AI торгова система, що дозволяє запускати кілька AI моделей для автоматичної торгівлі криптовалютними ф'ючерсами. Налаштовуйте стратегії через веб-інтерфейс, відстежуйте ефективність у реальному часі та дозвольте AI агентам конкурувати за найкращий торговий підхід.
|
||||
|
||||
### Основні функції
|
||||
|
||||
- **Мульти-AI підтримка**: Запускайте DeepSeek, Qwen, GPT, Claude, Gemini, Grok, Kimi — перемикайтеся між моделями будь-коли
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
<h1 align="center">NOFX — Hệ Điều Hành Giao Dịch AI Mã Nguồn Mở</h1>
|
||||
# NOFX - Hệ Thống Giao Dịch AI
|
||||
|
||||
<p align="center">
|
||||
<strong>Lớp cơ sở hạ tầng cho giao dịch tài chính AI-powered</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/NoFxAiOS/nofx/stargazers"><img src="https://img.shields.io/github/stars/NoFxAiOS/nofx?style=for-the-badge" alt="Stars"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/releases"><img src="https://img.shields.io/github/v/release/NoFxAiOS/nofx?style=for-the-badge" alt="Release"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-AGPL--3.0-blue.svg?style=for-the-badge" alt="License"></a>
|
||||
<a href="https://t.me/nofx_dev_community"><img src="https://img.shields.io/badge/Telegram-Community-blue?style=for-the-badge&logo=telegram" alt="Telegram"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://golang.org/"><img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go" alt="Go"></a>
|
||||
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
||||
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript" alt="TypeScript"></a>
|
||||
</p>
|
||||
[](https://golang.org/)
|
||||
[](https://reactjs.org/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](LICENSE)
|
||||
|
||||
**Ngôn ngữ:** [English](../../../README.md) | [中文](../zh-CN/README.md) | [Tiếng Việt](README.md)
|
||||
|
||||
---
|
||||
|
||||
## Nền Tảng Giao Dịch Crypto Sử Dụng AI
|
||||
|
||||
**NOFX** là hệ thống giao dịch AI mã nguồn mở cho phép bạn chạy nhiều mô hình AI để tự động giao dịch hợp đồng tương lai crypto. Cấu hình chiến lược qua giao diện web, theo dõi hiệu suất theo thời gian thực, và để các AI agent cạnh tranh tìm ra phương pháp giao dịch tốt nhất.
|
||||
|
||||
### Tính Năng Chính
|
||||
|
||||
- **Hỗ trợ Đa AI**: Chạy DeepSeek, Qwen, GPT, Claude, Gemini, Grok, Kimi - chuyển đổi mô hình bất cứ lúc nào
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
<h1 align="center">NOFX — 开源 AI 交易操作系统</h1>
|
||||
# NOFX - AI 交易系统
|
||||
|
||||
<p align="center">
|
||||
<strong>AI 驱动金融交易的基础设施层</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/NoFxAiOS/nofx/stargazers"><img src="https://img.shields.io/github/stars/NoFxAiOS/nofx?style=for-the-badge" alt="Stars"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/releases"><img src="https://img.shields.io/github/v/release/NoFxAiOS/nofx?style=for-the-badge" alt="Release"></a>
|
||||
<a href="https://github.com/NoFxAiOS/nofx/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-AGPL--3.0-blue.svg?style=for-the-badge" alt="License"></a>
|
||||
<a href="https://t.me/nofx_dev_community"><img src="https://img.shields.io/badge/Telegram-Community-blue?style=for-the-badge&logo=telegram" alt="Telegram"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://golang.org/"><img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go" alt="Go"></a>
|
||||
<a href="https://reactjs.org/"><img src="https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react" alt="React"></a>
|
||||
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript" alt="TypeScript"></a>
|
||||
</p>
|
||||
[](https://golang.org/)
|
||||
[](https://reactjs.org/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](LICENSE)
|
||||
|
||||
> **语言声明:** 本中文版本文档仅为方便海外华人社区阅读而提供,不代表本软件面向中国大陆、香港、澳门或台湾地区用户开放。如您位于上述地区,请勿使用本软件。
|
||||
|
||||
@@ -28,6 +16,10 @@
|
||||
|
||||
---
|
||||
|
||||
## AI 驱动的加密货币交易平台
|
||||
|
||||
**NOFX** 是一个开源的 AI 交易系统,让你可以运行多个 AI 模型自动交易加密货币期货。通过 Web 界面配置策略,实时监控表现,让多个 AI 代理竞争找出最佳交易方案。
|
||||
|
||||
### 核心功能
|
||||
|
||||
- **多 AI 支持**: 运行 DeepSeek、通义千问、GPT、Claude、Gemini、Grok、Kimi - 随时切换模型
|
||||
|
||||
18
go.mod
18
go.mod
@@ -5,19 +5,25 @@ go 1.25.3
|
||||
require (
|
||||
github.com/adshao/go-binance/v2 v2.8.9
|
||||
github.com/agiledragon/gomonkey/v2 v2.13.0
|
||||
github.com/bybit-exchange/bybit.go.api v0.0.0-20250727214011-c9347d6804d6
|
||||
github.com/elliottech/lighter-go v0.0.0-20251104171447-78b9b55ebc48
|
||||
github.com/ethereum/go-ethereum v1.16.7
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/sonirico/go-hyperliquid v0.26.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
golang.org/x/crypto v0.42.0
|
||||
golang.org/x/net v0.43.0
|
||||
gopkg.in/telebot.v3 v3.3.8
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
modernc.org/sqlite v1.40.0
|
||||
)
|
||||
|
||||
@@ -27,7 +33,6 @@ require (
|
||||
github.com/bitly/go-simplejson v0.5.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.0 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
github.com/bybit-exchange/bybit.go.api v0.0.0-20250727214011-c9347d6804d6 // indirect
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
@@ -39,7 +44,6 @@ require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/elastic/go-sysinfo v1.15.4 // indirect
|
||||
github.com/elastic/go-windows v1.0.2 // indirect
|
||||
github.com/elliottech/lighter-go v0.0.0-20251104171447-78b9b55ebc48 // indirect
|
||||
github.com/elliottech/poseidon_crypto v0.0.11 // indirect
|
||||
github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect
|
||||
github.com/ethereum/go-verkle v0.2.2 // indirect
|
||||
@@ -50,6 +54,7 @@ require (
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/goccy/go-json v0.10.4 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/holiman/uint256 v1.3.2 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
@@ -62,7 +67,6 @@ require (
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/mailru/easyjson v0.9.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -93,16 +97,12 @@ require (
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gorm.io/driver/postgres v1.6.0 // indirect
|
||||
gorm.io/driver/sqlite v1.6.0 // indirect
|
||||
gorm.io/gorm v1.31.1 // indirect
|
||||
howett.net/plist v1.0.1 // indirect
|
||||
modernc.org/libc v1.66.10 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
|
||||
102
main.go
102
main.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"nofx/api"
|
||||
"nofx/assistant"
|
||||
"nofx/auth"
|
||||
"nofx/backtest"
|
||||
"nofx/config"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"nofx/manager"
|
||||
"nofx/mcp"
|
||||
"nofx/store"
|
||||
"nofx/telegram"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
@@ -136,6 +138,52 @@ func main() {
|
||||
}
|
||||
}()
|
||||
|
||||
// Initialize and start Telegram bot (if configured)
|
||||
var telegramBot *telegram.Bot
|
||||
telegramConfig := telegram.LoadConfigFromEnv()
|
||||
if telegramConfig.Token != "" {
|
||||
logger.Info("🤖 Initializing Smart Trading Assistant...")
|
||||
|
||||
// Create AI client for the assistant
|
||||
aiClient := createAssistantAIClient()
|
||||
if aiClient == nil {
|
||||
logger.Error("❌ No AI API key configured, Telegram bot disabled")
|
||||
} else {
|
||||
// Create Smart AI Agent with trading context awareness
|
||||
agentConfig := assistant.DefaultAgentConfig()
|
||||
smartAgent := assistant.NewSmartAgent(aiClient, agentConfig, traderManager, st)
|
||||
|
||||
// Register trading tools
|
||||
tradingTools := assistant.NewTradingTools(traderManager, st)
|
||||
smartAgent.RegisterTools(tradingTools.GetAllTools()...)
|
||||
|
||||
// Register strategy tools
|
||||
strategyTools := assistant.NewStrategyTools(st)
|
||||
smartAgent.RegisterTools(strategyTools.GetAllTools()...)
|
||||
|
||||
// Create and start Telegram bot
|
||||
var err error
|
||||
telegramBot, err = telegram.NewBot(telegramConfig, smartAgent.Agent)
|
||||
if err != nil {
|
||||
logger.Errorf("❌ Failed to create Telegram bot: %v", err)
|
||||
} else {
|
||||
// Start background monitor with alert forwarding to Telegram
|
||||
smartAgent.OnAlert(func(alert assistant.Alert) {
|
||||
telegramBot.BroadcastAlert(alert.Message)
|
||||
})
|
||||
smartAgent.StartMonitor()
|
||||
|
||||
go telegramBot.Start()
|
||||
logger.Info("✅ Smart Trading Assistant started successfully")
|
||||
logger.Info(" 📊 Real-time context injection: enabled")
|
||||
logger.Info(" 🔍 Background monitoring: enabled")
|
||||
logger.Info(" ⚠️ Proactive alerts: enabled")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.Info("ℹ️ Telegram bot not configured (set TELEGRAM_BOT_TOKEN to enable)")
|
||||
}
|
||||
|
||||
// Wait for interrupt signal
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
@@ -146,6 +194,11 @@ func main() {
|
||||
<-quit
|
||||
logger.Info("📴 Shutdown signal received, closing system...")
|
||||
|
||||
// Stop Telegram bot
|
||||
if telegramBot != nil {
|
||||
telegramBot.Stop()
|
||||
}
|
||||
|
||||
// Stop all traders
|
||||
traderManager.StopAll()
|
||||
logger.Info("✅ System shut down safely")
|
||||
@@ -161,6 +214,55 @@ func newSharedMCPClient() mcp.AIClient {
|
||||
return mcp.NewDeepSeekClient()
|
||||
}
|
||||
|
||||
// createAssistantAIClient creates an AI client for the Telegram assistant
|
||||
// Supports multiple providers based on environment configuration
|
||||
func createAssistantAIClient() mcp.AIClient {
|
||||
// Try different providers in order of preference
|
||||
|
||||
// 1. DeepSeek (cost-effective, recommended)
|
||||
if apiKey := os.Getenv("DEEPSEEK_API_KEY"); apiKey != "" {
|
||||
client := mcp.NewDeepSeekClient()
|
||||
customURL := os.Getenv("DEEPSEEK_API_URL")
|
||||
customModel := os.Getenv("DEEPSEEK_MODEL")
|
||||
client.SetAPIKey(apiKey, customURL, customModel)
|
||||
logger.Info("🧠 Assistant using DeepSeek AI")
|
||||
return client
|
||||
}
|
||||
|
||||
// 2. Claude
|
||||
if apiKey := os.Getenv("CLAUDE_API_KEY"); apiKey != "" {
|
||||
client := mcp.NewClaudeClient()
|
||||
customURL := os.Getenv("CLAUDE_API_URL")
|
||||
customModel := os.Getenv("CLAUDE_MODEL")
|
||||
client.SetAPIKey(apiKey, customURL, customModel)
|
||||
logger.Info("🧠 Assistant using Claude AI")
|
||||
return client
|
||||
}
|
||||
|
||||
// 3. OpenAI
|
||||
if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
|
||||
client := mcp.NewOpenAIClient()
|
||||
customURL := os.Getenv("OPENAI_API_URL")
|
||||
customModel := os.Getenv("OPENAI_MODEL")
|
||||
client.SetAPIKey(apiKey, customURL, customModel)
|
||||
logger.Info("🧠 Assistant using OpenAI")
|
||||
return client
|
||||
}
|
||||
|
||||
// 4. Qwen
|
||||
if apiKey := os.Getenv("QWEN_API_KEY"); apiKey != "" {
|
||||
client := mcp.NewQwenClient()
|
||||
customURL := os.Getenv("QWEN_API_URL")
|
||||
customModel := os.Getenv("QWEN_MODEL")
|
||||
client.SetAPIKey(apiKey, customURL, customModel)
|
||||
logger.Info("🧠 Assistant using Qwen AI")
|
||||
return client
|
||||
}
|
||||
|
||||
logger.Warn("⚠️ No AI API key configured for assistant")
|
||||
return nil
|
||||
}
|
||||
|
||||
// initInstallationID initializes the anonymous installation ID for experience improvement
|
||||
// This ID is persisted in database and used for anonymous usage statistics
|
||||
func initInstallationID(st *store.Store) {
|
||||
|
||||
490
telegram/bot.go
Normal file
490
telegram/bot.go
Normal file
@@ -0,0 +1,490 @@
|
||||
// Package telegram provides Telegram bot integration for NOFX trading assistant
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"nofx/assistant"
|
||||
"nofx/logger"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
tele "gopkg.in/telebot.v3"
|
||||
)
|
||||
|
||||
// Bot represents the Telegram bot for NOFX
|
||||
type Bot struct {
|
||||
bot *tele.Bot
|
||||
agent *assistant.Agent
|
||||
config BotConfig
|
||||
|
||||
// Allowed users (for security)
|
||||
allowedUsers map[int64]bool
|
||||
allowedUsersLock sync.RWMutex
|
||||
|
||||
// Rate limiting
|
||||
rateLimiter *RateLimiter
|
||||
}
|
||||
|
||||
// BotConfig holds bot configuration
|
||||
type BotConfig struct {
|
||||
Token string `json:"token"`
|
||||
|
||||
// Polling or webhook mode
|
||||
UseWebhook bool `json:"use_webhook"`
|
||||
WebhookURL string `json:"webhook_url"`
|
||||
WebhookPort int `json:"webhook_port"`
|
||||
|
||||
// Security
|
||||
AllowedUserIDs []int64 `json:"allowed_user_ids"` // Empty = allow all
|
||||
AdminUserIDs []int64 `json:"admin_user_ids"`
|
||||
|
||||
// Rate limiting
|
||||
MaxMessagesPerMinute int `json:"max_messages_per_minute"`
|
||||
|
||||
// Language
|
||||
DefaultLanguage string `json:"default_language"` // "en" or "zh"
|
||||
}
|
||||
|
||||
// DefaultBotConfig returns default configuration
|
||||
func DefaultBotConfig() BotConfig {
|
||||
return BotConfig{
|
||||
MaxMessagesPerMinute: 30,
|
||||
DefaultLanguage: "zh",
|
||||
}
|
||||
}
|
||||
|
||||
// NewBot creates a new Telegram bot
|
||||
func NewBot(config BotConfig, agent *assistant.Agent) (*Bot, error) {
|
||||
if config.Token == "" {
|
||||
return nil, fmt.Errorf("telegram bot token is required")
|
||||
}
|
||||
|
||||
settings := tele.Settings{
|
||||
Token: config.Token,
|
||||
Poller: &tele.LongPoller{Timeout: 30 * time.Second},
|
||||
}
|
||||
|
||||
teleBot, err := tele.NewBot(settings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create telegram bot: %w", err)
|
||||
}
|
||||
|
||||
bot := &Bot{
|
||||
bot: teleBot,
|
||||
agent: agent,
|
||||
config: config,
|
||||
allowedUsers: make(map[int64]bool),
|
||||
rateLimiter: NewRateLimiter(config.MaxMessagesPerMinute),
|
||||
}
|
||||
|
||||
// Initialize allowed users
|
||||
for _, uid := range config.AllowedUserIDs {
|
||||
bot.allowedUsers[uid] = true
|
||||
}
|
||||
|
||||
// Register handlers
|
||||
bot.registerHandlers()
|
||||
|
||||
return bot, nil
|
||||
}
|
||||
|
||||
// Start starts the bot
|
||||
func (b *Bot) Start() {
|
||||
logger.Info("🤖 Starting Telegram bot...")
|
||||
b.bot.Start()
|
||||
}
|
||||
|
||||
// Stop stops the bot
|
||||
func (b *Bot) Stop() {
|
||||
logger.Info("🤖 Stopping Telegram bot...")
|
||||
b.bot.Stop()
|
||||
}
|
||||
|
||||
// registerHandlers sets up all message handlers
|
||||
func (b *Bot) registerHandlers() {
|
||||
// Middleware for access control and rate limiting
|
||||
b.bot.Use(b.accessControlMiddleware)
|
||||
b.bot.Use(b.rateLimitMiddleware)
|
||||
|
||||
// Command handlers
|
||||
b.bot.Handle("/start", b.handleStart)
|
||||
b.bot.Handle("/help", b.handleHelp)
|
||||
b.bot.Handle("/status", b.handleStatus)
|
||||
b.bot.Handle("/balance", b.handleBalance)
|
||||
b.bot.Handle("/positions", b.handlePositions)
|
||||
b.bot.Handle("/traders", b.handleTraders)
|
||||
b.bot.Handle("/clear", b.handleClear)
|
||||
|
||||
// Handle all text messages (send to AI agent)
|
||||
b.bot.Handle(tele.OnText, b.handleText)
|
||||
|
||||
// Handle callbacks (for inline keyboards)
|
||||
b.bot.Handle(tele.OnCallback, b.handleCallback)
|
||||
|
||||
logger.Info("✅ Telegram handlers registered")
|
||||
}
|
||||
|
||||
// accessControlMiddleware checks if user is allowed
|
||||
func (b *Bot) accessControlMiddleware(next tele.HandlerFunc) tele.HandlerFunc {
|
||||
return func(c tele.Context) error {
|
||||
userID := c.Sender().ID
|
||||
|
||||
// If allowlist is empty, allow all
|
||||
if len(b.config.AllowedUserIDs) == 0 {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
b.allowedUsersLock.RLock()
|
||||
allowed := b.allowedUsers[userID]
|
||||
b.allowedUsersLock.RUnlock()
|
||||
|
||||
if !allowed {
|
||||
logger.Warnf("⚠️ Unauthorized access attempt from user %d", userID)
|
||||
return c.Send("⛔ Sorry, you are not authorized to use this bot.\n\n抱歉,您没有使用此机器人的权限。")
|
||||
}
|
||||
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
// rateLimitMiddleware implements rate limiting
|
||||
func (b *Bot) rateLimitMiddleware(next tele.HandlerFunc) tele.HandlerFunc {
|
||||
return func(c tele.Context) error {
|
||||
userID := c.Sender().ID
|
||||
|
||||
if !b.rateLimiter.Allow(userID) {
|
||||
return c.Send("⏳ Please slow down. Too many messages.\n\n请稍等,消息发送过于频繁。")
|
||||
}
|
||||
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Command Handlers ====================
|
||||
|
||||
func (b *Bot) handleStart(c tele.Context) error {
|
||||
welcome := `🚀 *Welcome to NOFX Trading Assistant!*
|
||||
|
||||
I'm your AI-powered trading assistant. I can help you:
|
||||
|
||||
📊 *Monitor* - Check balances, positions, and market prices
|
||||
🤖 *Manage* - Start/stop AI traders, configure strategies
|
||||
💹 *Trade* - Execute trades (with confirmation)
|
||||
📈 *Analyze* - Market analysis and AI debates
|
||||
|
||||
*Commands:*
|
||||
/help - Show all commands
|
||||
/status - System status
|
||||
/balance - Account balances
|
||||
/positions - Current positions
|
||||
/traders - List AI traders
|
||||
/clear - Clear conversation history
|
||||
|
||||
Or just chat with me in natural language!
|
||||
|
||||
---
|
||||
|
||||
🚀 *欢迎使用 NOFX 交易助手!*
|
||||
|
||||
我是你的 AI 交易助手,可以帮你:
|
||||
|
||||
📊 *监控* - 查看余额、持仓、行情
|
||||
🤖 *管理* - 启停 AI 交易员、配置策略
|
||||
💹 *交易* - 执行交易(需确认)
|
||||
📈 *分析* - 市场分析和 AI 辩论
|
||||
|
||||
直接用自然语言和我对话即可!`
|
||||
|
||||
return c.Send(welcome, tele.ModeMarkdown)
|
||||
}
|
||||
|
||||
func (b *Bot) handleHelp(c tele.Context) error {
|
||||
help := `📖 *NOFX Trading Assistant Help*
|
||||
|
||||
*Commands:*
|
||||
• /start - Welcome message
|
||||
• /help - This help message
|
||||
• /status - System overview
|
||||
• /balance - Show all balances
|
||||
• /positions - Show all positions
|
||||
• /traders - List AI traders
|
||||
• /clear - Clear conversation history
|
||||
|
||||
*Natural Language Examples:*
|
||||
• "查看我的余额"
|
||||
• "BTC 现在多少钱"
|
||||
• "启动交易员 xxx"
|
||||
• "帮我平掉 ETH 的多单"
|
||||
• "我的持仓盈亏怎么样"
|
||||
• "列出所有策略"
|
||||
|
||||
*Tips:*
|
||||
• I'll always confirm before executing trades
|
||||
• Use specific trader names/IDs for operations
|
||||
• Ask me anything about your trading!`
|
||||
|
||||
return c.Send(help, tele.ModeMarkdown)
|
||||
}
|
||||
|
||||
func (b *Bot) handleStatus(c tele.Context) error {
|
||||
ctx := context.Background()
|
||||
sessionID := b.getSessionID(c)
|
||||
|
||||
response, err := b.agent.Chat(ctx, sessionID, "Please give me a brief system status: list all traders and their status, show total positions count.")
|
||||
if err != nil {
|
||||
logger.Errorf("Agent error: %v", err)
|
||||
return c.Send("❌ Failed to get status. Please try again.")
|
||||
}
|
||||
|
||||
return c.Send(response.Text, tele.ModeMarkdown)
|
||||
}
|
||||
|
||||
func (b *Bot) handleBalance(c tele.Context) error {
|
||||
ctx := context.Background()
|
||||
sessionID := b.getSessionID(c)
|
||||
|
||||
response, err := b.agent.Chat(ctx, sessionID, "Show me all account balances from all running traders.")
|
||||
if err != nil {
|
||||
logger.Errorf("Agent error: %v", err)
|
||||
return c.Send("❌ Failed to get balances. Please try again.")
|
||||
}
|
||||
|
||||
return c.Send(response.Text, tele.ModeMarkdown)
|
||||
}
|
||||
|
||||
func (b *Bot) handlePositions(c tele.Context) error {
|
||||
ctx := context.Background()
|
||||
sessionID := b.getSessionID(c)
|
||||
|
||||
response, err := b.agent.Chat(ctx, sessionID, "Show me all current positions from all running traders with P&L.")
|
||||
if err != nil {
|
||||
logger.Errorf("Agent error: %v", err)
|
||||
return c.Send("❌ Failed to get positions. Please try again.")
|
||||
}
|
||||
|
||||
return c.Send(response.Text, tele.ModeMarkdown)
|
||||
}
|
||||
|
||||
func (b *Bot) handleTraders(c tele.Context) error {
|
||||
ctx := context.Background()
|
||||
sessionID := b.getSessionID(c)
|
||||
|
||||
response, err := b.agent.Chat(ctx, sessionID, "List all configured AI traders with their status, exchange, and AI model.")
|
||||
if err != nil {
|
||||
logger.Errorf("Agent error: %v", err)
|
||||
return c.Send("❌ Failed to list traders. Please try again.")
|
||||
}
|
||||
|
||||
return c.Send(response.Text, tele.ModeMarkdown)
|
||||
}
|
||||
|
||||
func (b *Bot) handleClear(c tele.Context) error {
|
||||
sessionID := b.getSessionID(c)
|
||||
session := b.agent.GetSession(sessionID)
|
||||
session.Clear()
|
||||
|
||||
return c.Send("🧹 Conversation history cleared.\n\n对话历史已清除。")
|
||||
}
|
||||
|
||||
// ==================== Message Handler ====================
|
||||
|
||||
func (b *Bot) handleText(c tele.Context) error {
|
||||
text := strings.TrimSpace(c.Text())
|
||||
if text == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Show typing indicator
|
||||
_ = c.Notify(tele.Typing)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancel()
|
||||
|
||||
sessionID := b.getSessionID(c)
|
||||
|
||||
// Set user info in session
|
||||
session := b.agent.GetSession(sessionID)
|
||||
session.SetUserInfo(
|
||||
strconv.FormatInt(c.Sender().ID, 10),
|
||||
c.Sender().Username,
|
||||
"telegram",
|
||||
)
|
||||
|
||||
logger.Infof("💬 [%s] %s: %s", sessionID, c.Sender().Username, text)
|
||||
|
||||
response, err := b.agent.Chat(ctx, sessionID, text)
|
||||
if err != nil {
|
||||
logger.Errorf("Agent error: %v", err)
|
||||
return c.Send("❌ Sorry, something went wrong. Please try again.\n\n抱歉,出现了问题,请重试。")
|
||||
}
|
||||
|
||||
logger.Infof("🤖 [%s] Response: %s", sessionID, truncate(response.Text, 100))
|
||||
|
||||
// Send response (split if too long)
|
||||
return b.sendLongMessage(c, response.Text)
|
||||
}
|
||||
|
||||
// handleCallback handles inline keyboard callbacks
|
||||
func (b *Bot) handleCallback(c tele.Context) error {
|
||||
data := c.Callback().Data
|
||||
|
||||
// Parse callback data (format: "action:param1:param2")
|
||||
parts := strings.Split(data, ":")
|
||||
if len(parts) == 0 {
|
||||
return c.Respond()
|
||||
}
|
||||
|
||||
action := parts[0]
|
||||
|
||||
switch action {
|
||||
case "confirm_trade":
|
||||
if len(parts) >= 2 {
|
||||
// Execute the confirmed trade
|
||||
return b.executeConfirmedTrade(c, parts[1:])
|
||||
}
|
||||
case "cancel_trade":
|
||||
_ = c.Respond(&tele.CallbackResponse{Text: "Trade cancelled / 交易已取消"})
|
||||
return c.Edit("❌ Trade cancelled.\n\n交易已取消。")
|
||||
}
|
||||
|
||||
return c.Respond()
|
||||
}
|
||||
|
||||
// ==================== Helpers ====================
|
||||
|
||||
func (b *Bot) getSessionID(c tele.Context) string {
|
||||
return fmt.Sprintf("tg_%d", c.Chat().ID)
|
||||
}
|
||||
|
||||
func (b *Bot) sendLongMessage(c tele.Context, text string) error {
|
||||
// Telegram message limit is 4096 characters
|
||||
const maxLen = 4000
|
||||
|
||||
if len(text) <= maxLen {
|
||||
return c.Send(text, tele.ModeMarkdown)
|
||||
}
|
||||
|
||||
// Split into chunks
|
||||
for len(text) > 0 {
|
||||
chunk := text
|
||||
if len(chunk) > maxLen {
|
||||
// Try to split at newline
|
||||
idx := strings.LastIndex(text[:maxLen], "\n")
|
||||
if idx > 0 {
|
||||
chunk = text[:idx]
|
||||
text = text[idx+1:]
|
||||
} else {
|
||||
chunk = text[:maxLen]
|
||||
text = text[maxLen:]
|
||||
}
|
||||
} else {
|
||||
text = ""
|
||||
}
|
||||
|
||||
if err := c.Send(chunk, tele.ModeMarkdown); err != nil {
|
||||
// Try without markdown if it fails
|
||||
if err := c.Send(chunk); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bot) executeConfirmedTrade(c tele.Context, params []string) error {
|
||||
// TODO: Implement trade execution from callback
|
||||
_ = c.Respond(&tele.CallbackResponse{Text: "Executing trade..."})
|
||||
return c.Edit("✅ Trade executed.\n\n交易已执行。")
|
||||
}
|
||||
|
||||
// AddAllowedUser adds a user to the allowlist
|
||||
func (b *Bot) AddAllowedUser(userID int64) {
|
||||
b.allowedUsersLock.Lock()
|
||||
defer b.allowedUsersLock.Unlock()
|
||||
b.allowedUsers[userID] = true
|
||||
}
|
||||
|
||||
// BroadcastAlert sends an alert to all admin users
|
||||
func (b *Bot) BroadcastAlert(message string) {
|
||||
for _, adminID := range b.config.AdminUserIDs {
|
||||
chat := &tele.Chat{ID: adminID}
|
||||
_, err := b.bot.Send(chat, "🚨 "+message)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to send alert to admin %d: %v", adminID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// If no admins configured, send to allowed users
|
||||
if len(b.config.AdminUserIDs) == 0 {
|
||||
for userID := range b.allowedUsers {
|
||||
chat := &tele.Chat{ID: userID}
|
||||
_, _ = b.bot.Send(chat, "🚨 "+message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveAllowedUser removes a user from the allowlist
|
||||
func (b *Bot) RemoveAllowedUser(userID int64) {
|
||||
b.allowedUsersLock.Lock()
|
||||
defer b.allowedUsersLock.Unlock()
|
||||
delete(b.allowedUsers, userID)
|
||||
}
|
||||
|
||||
func truncate(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen] + "..."
|
||||
}
|
||||
|
||||
// ==================== Rate Limiter ====================
|
||||
|
||||
// RateLimiter implements per-user rate limiting
|
||||
type RateLimiter struct {
|
||||
maxPerMinute int
|
||||
users map[int64][]time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewRateLimiter creates a new rate limiter
|
||||
func NewRateLimiter(maxPerMinute int) *RateLimiter {
|
||||
return &RateLimiter{
|
||||
maxPerMinute: maxPerMinute,
|
||||
users: make(map[int64][]time.Time),
|
||||
}
|
||||
}
|
||||
|
||||
// Allow checks if a user is allowed to send a message
|
||||
func (r *RateLimiter) Allow(userID int64) bool {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
cutoff := now.Add(-time.Minute)
|
||||
|
||||
// Get user's recent messages
|
||||
timestamps := r.users[userID]
|
||||
|
||||
// Filter out old timestamps
|
||||
var recent []time.Time
|
||||
for _, t := range timestamps {
|
||||
if t.After(cutoff) {
|
||||
recent = append(recent, t)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if under limit
|
||||
if len(recent) >= r.maxPerMinute {
|
||||
return false
|
||||
}
|
||||
|
||||
// Add current timestamp
|
||||
recent = append(recent, now)
|
||||
r.users[userID] = recent
|
||||
|
||||
return true
|
||||
}
|
||||
60
telegram/config.go
Normal file
60
telegram/config.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LoadConfigFromEnv loads Telegram bot configuration from environment variables
|
||||
func LoadConfigFromEnv() BotConfig {
|
||||
config := DefaultBotConfig()
|
||||
|
||||
// Bot token (required)
|
||||
config.Token = os.Getenv("TELEGRAM_BOT_TOKEN")
|
||||
|
||||
// Webhook settings
|
||||
if webhook := os.Getenv("TELEGRAM_WEBHOOK_URL"); webhook != "" {
|
||||
config.UseWebhook = true
|
||||
config.WebhookURL = webhook
|
||||
}
|
||||
if port := os.Getenv("TELEGRAM_WEBHOOK_PORT"); port != "" {
|
||||
if p, err := strconv.Atoi(port); err == nil {
|
||||
config.WebhookPort = p
|
||||
}
|
||||
}
|
||||
|
||||
// Allowed users (comma-separated list of user IDs)
|
||||
if allowedStr := os.Getenv("TELEGRAM_ALLOWED_USERS"); allowedStr != "" {
|
||||
for _, idStr := range strings.Split(allowedStr, ",") {
|
||||
idStr = strings.TrimSpace(idStr)
|
||||
if id, err := strconv.ParseInt(idStr, 10, 64); err == nil {
|
||||
config.AllowedUserIDs = append(config.AllowedUserIDs, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Admin users
|
||||
if adminStr := os.Getenv("TELEGRAM_ADMIN_USERS"); adminStr != "" {
|
||||
for _, idStr := range strings.Split(adminStr, ",") {
|
||||
idStr = strings.TrimSpace(idStr)
|
||||
if id, err := strconv.ParseInt(idStr, 10, 64); err == nil {
|
||||
config.AdminUserIDs = append(config.AdminUserIDs, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
if rateStr := os.Getenv("TELEGRAM_RATE_LIMIT"); rateStr != "" {
|
||||
if rate, err := strconv.Atoi(rateStr); err == nil {
|
||||
config.MaxMessagesPerMinute = rate
|
||||
}
|
||||
}
|
||||
|
||||
// Language
|
||||
if lang := os.Getenv("TELEGRAM_LANGUAGE"); lang != "" {
|
||||
config.DefaultLanguage = lang
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
Reference in New Issue
Block a user