diff --git a/assistant/context.go b/assistant/context.go new file mode 100644 index 00000000..e2379127 --- /dev/null +++ b/assistant/context.go @@ -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 +} diff --git a/assistant/monitor.go b/assistant/monitor.go new file mode 100644 index 00000000..ab65403b --- /dev/null +++ b/assistant/monitor.go @@ -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 +} diff --git a/assistant/smart_agent.go b/assistant/smart_agent.go new file mode 100644 index 00000000..926f1f2f --- /dev/null +++ b/assistant/smart_agent.go @@ -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() +} diff --git a/assistant/smart_prompts.go b/assistant/smart_prompts.go new file mode 100644 index 00000000..a80ea7e6 --- /dev/null +++ b/assistant/smart_prompts.go @@ -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) +} diff --git a/main.go b/main.go index 1ae8f74f..8bd030c9 100644 --- a/main.go +++ b/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)") diff --git a/telegram/bot.go b/telegram/bot.go index 28b5fb1f..a04c4fa0 100644 --- a/telegram/bot.go +++ b/telegram/bot.go @@ -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()