package main import ( "bytes" "encoding/json" "fmt" "log" "net/http" "time" ) type Notifier struct { BotToken string ChatID string client *http.Client } func NewNotifier(botToken, chatID string) *Notifier { return &Notifier{ BotToken: botToken, ChatID: chatID, client: &http.Client{Timeout: 10 * time.Second}, } } // Send sends a text message to Telegram. func (n *Notifier) Send(text string) error { if n.BotToken == "" || n.ChatID == "" { log.Printf("[Notifier] Skipped (not configured): %.80s", text) return nil } url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", n.BotToken) payload := map[string]string{ "chat_id": n.ChatID, "text": text, "parse_mode": "HTML", } body, _ := json.Marshal(payload) resp, err := n.client.Post(url, "application/json", bytes.NewReader(body)) if err != nil { return fmt.Errorf("telegram send error: %w", err) } defer resp.Body.Close() if resp.StatusCode != 200 { return fmt.Errorf("telegram status %d", resp.StatusCode) } log.Printf("[Notifier] Sent (%d bytes)", len(text)) return nil } // SendAlert sends an arbitrage alert notification. func (n *Notifier) SendAlert(opp *ArbOpportunity) { msg := fmt.Sprintf( "[套利信号] %s/USDT\n"+ " %s %.4f -> %s %.4f\n"+ " 净利: %+.4f%%\n", opp.Coin, opp.BuyEx, opp.BuyPrice, opp.SellEx, opp.SellPrice, opp.NetProfit, ) if opp.NetProfit > 0.10 { msg += " 高价值机会!\n" } if err := n.Send(msg); err != nil { log.Printf("[Notifier] Alert error: %v", err) } } // SendTradeSummary sends a summary of open positions at each hour. func (n *Notifier) SendTradeSummary(positions []ArbPosition, timeStr string) { if n.BotToken == "" || n.ChatID == "" { return } lines := fmt.Sprintf("=== 持仓汇总 === %s\n", timeStr) if len(positions) == 0 { lines += " 当前无持仓\n" } else { for i, p := range positions { dur := time.Since(p.StartedAt).Round(time.Second).String() lines += fmt.Sprintf("%d. %s %s %.0f %s\n", i+1, p.Coin, p.Direction, p.AmountUSD, dur) } } if err := n.Send(lines); err != nil { log.Printf("[Notifier] Hourly error: %v", err) } }