mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-09 22:10:57 +08:00
feat: Add smart trading assistant with context awareness
- Add SmartAgent with automatic context injection - Real-time portfolio/position data in every prompt - AI knows current state before responding - Add TradingContext builder - Aggregates balance, positions, P&L across all traders - Auto-generates alerts (liquidation risk, large loss, etc.) - Add background Monitor - Proactive position monitoring every 30s - Detects new positions, closed positions - Forwards alerts to Telegram - Enhanced system prompts - Professional trading assistant persona - Risk assessment guidelines - Clear response formatting rules Features: ✅ Context-aware responses ✅ Proactive risk alerts ✅ Background monitoring ✅ Alert broadcasting to Telegram
This commit is contained in:
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
|
||||
}
|
||||
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)
|
||||
}
|
||||
51
main.go
51
main.go
@@ -142,32 +142,39 @@ func main() {
|
||||
var telegramBot *telegram.Bot
|
||||
telegramConfig := telegram.LoadConfigFromEnv()
|
||||
if telegramConfig.Token != "" {
|
||||
logger.Info("🤖 Initializing Telegram AI Assistant...")
|
||||
logger.Info("🤖 Initializing Smart Trading Assistant...")
|
||||
|
||||
// Create AI client for the assistant
|
||||
aiClient := createAssistantAIClient()
|
||||
|
||||
// Create AI Agent with trading tools
|
||||
agentConfig := assistant.DefaultAgentConfig()
|
||||
agent := assistant.NewAgent(aiClient, agentConfig)
|
||||
|
||||
// Register trading tools
|
||||
tradingTools := assistant.NewTradingTools(traderManager, st)
|
||||
agent.RegisterTools(tradingTools.GetAllTools()...)
|
||||
|
||||
// Set system prompt based on language
|
||||
if telegramConfig.DefaultLanguage == "zh" {
|
||||
agent.SetSystemPrompt(assistant.ChineseSystemPrompt())
|
||||
}
|
||||
|
||||
// Create and start Telegram bot
|
||||
var err error
|
||||
telegramBot, err = telegram.NewBot(telegramConfig, agent)
|
||||
if err != nil {
|
||||
logger.Errorf("❌ Failed to create Telegram bot: %v", err)
|
||||
if aiClient == nil {
|
||||
logger.Error("❌ No AI API key configured, Telegram bot disabled")
|
||||
} else {
|
||||
go telegramBot.Start()
|
||||
logger.Info("✅ Telegram AI Assistant started successfully")
|
||||
// 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()...)
|
||||
|
||||
// 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)")
|
||||
|
||||
@@ -408,6 +408,25 @@ func (b *Bot) AddAllowedUser(userID int64) {
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user