change v1

This commit is contained in:
lky-spec
2026-04-25 16:18:45 +08:00
parent 737f9bca95
commit c244e4cdf1
89 changed files with 17382 additions and 6198 deletions

View File

@@ -41,6 +41,7 @@ type Sentinel struct {
http *http.Client
logger *slog.Logger
stopCh chan struct{}
stopOnce sync.Once
}
type pricePt struct {
@@ -76,20 +77,51 @@ func (s *Sentinel) Start() {
})
}
func (s *Sentinel) Stop() { close(s.stopCh) }
func (s *Sentinel) SymbolCount() int { s.mu.RLock(); defer s.mu.RUnlock(); return len(s.symbols) }
func (s *Sentinel) AddSymbol(sym string) { s.mu.Lock(); defer s.mu.Unlock(); for _, x := range s.symbols { if x == sym { return } }; s.symbols = append(s.symbols, sym) }
func (s *Sentinel) RemoveSymbol(sym string) { s.mu.Lock(); defer s.mu.Unlock(); for i, x := range s.symbols { if x == sym { s.symbols = append(s.symbols[:i], s.symbols[i+1:]...); return } } }
func (s *Sentinel) Stop() { s.stopOnce.Do(func() { close(s.stopCh) }) }
func (s *Sentinel) SymbolCount() int { s.mu.RLock(); defer s.mu.RUnlock(); return len(s.symbols) }
func (s *Sentinel) Symbols() []string {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]string, len(s.symbols))
copy(out, s.symbols)
return out
}
func (s *Sentinel) AddSymbol(sym string) {
s.mu.Lock()
defer s.mu.Unlock()
for _, x := range s.symbols {
if x == sym {
return
}
}
s.symbols = append(s.symbols, sym)
}
func (s *Sentinel) RemoveSymbol(sym string) {
s.mu.Lock()
defer s.mu.Unlock()
for i, x := range s.symbols {
if x == sym {
s.symbols = append(s.symbols[:i], s.symbols[i+1:]...)
return
}
}
}
func (s *Sentinel) FormatWatchlist(L string) string {
s.mu.RLock()
defer s.mu.RUnlock()
if len(s.symbols) == 0 {
if L == "zh" { return "📭 监控列表为空。用 `/watch BTC` 添加。" }
if L == "zh" {
return "📭 监控列表为空。用 `/watch BTC` 添加。"
}
return "📭 Watchlist empty. Use `/watch BTC` to add."
}
var sb strings.Builder
if L == "zh" { sb.WriteString("👁️ *监控列表*\n\n") } else { sb.WriteString("👁️ *Watchlist*\n\n") }
if L == "zh" {
sb.WriteString("👁️ *监控列表*\n\n")
} else {
sb.WriteString("👁️ *Watchlist*\n\n")
}
for _, sym := range s.symbols {
if pts, ok := s.history[sym]; ok && len(pts) > 0 {
last := pts[len(pts)-1]
@@ -113,16 +145,22 @@ func (s *Sentinel) scan() {
func (s *Sentinel) check(symbol string) {
resp, err := s.http.Get(fmt.Sprintf("https://fapi.binance.com/fapi/v1/ticker/24hr?symbol=%s", symbol))
if err != nil { return }
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
s.logger.Debug("sentinel ticker non-200", "symbol", symbol, "status", resp.StatusCode)
return
}
body, err := safe.ReadAllLimited(resp.Body, 256*1024) // 256KB limit
if err != nil { return }
if err != nil {
return
}
var t map[string]interface{}
if err := json.Unmarshal(body, &t); err != nil { return }
if err := json.Unmarshal(body, &t); err != nil {
return
}
price, _ := strconv.ParseFloat(fmt.Sprint(t["lastPrice"]), 64)
vol, _ := strconv.ParseFloat(fmt.Sprint(t["quoteVolume"]), 64)
@@ -132,41 +170,53 @@ func (s *Sentinel) check(symbol string) {
s.mu.Lock()
h := s.history[symbol]
h = append(h, pt)
if len(h) > 60 { h = h[len(h)-60:] }
if len(h) > 60 {
h = h[len(h)-60:]
}
s.history[symbol] = h
s.mu.Unlock()
if len(h) < 5 { return }
if len(h) < 5 {
return
}
// Price breakout (>3% in 5 min)
old := h[len(h)-5]
pct := ((price - old.Price) / old.Price) * 100
if math.Abs(pct) >= 3.0 {
sev := "warning"
if math.Abs(pct) >= 6.0 { sev = "critical" }
if math.Abs(pct) >= 6.0 {
sev = "critical"
}
dir := "📈 拉升"
if pct < 0 { dir = "📉 下跌" }
if pct < 0 {
dir = "📉 下跌"
}
s.emit(Signal{Type: SignalPriceBreakout, Symbol: symbol, Severity: sev,
Title: fmt.Sprintf("%s %s %.1f%%", symbol, dir, math.Abs(pct)),
Title: fmt.Sprintf("%s %s %.1f%%", symbol, dir, math.Abs(pct)),
Detail: fmt.Sprintf("5min: $%.2f → $%.2f (24h: %.1f%%)", old.Price, price, chg),
Price: price, Change: pct})
Price: price, Change: pct})
}
// Volume spike (>3x avg)
if len(h) >= 10 {
var avg float64
for i := 0; i < len(h)-1; i++ { avg += h[i].Volume }
for i := 0; i < len(h)-1; i++ {
avg += h[i].Volume
}
avg /= float64(len(h) - 1)
if avg > 0 && vol > avg*3 {
s.emit(Signal{Type: SignalVolumeSpike, Symbol: symbol, Severity: "warning",
Title: fmt.Sprintf("%s 成交量异常 %.1fx", symbol, vol/avg),
Title: fmt.Sprintf("%s 成交量异常 %.1fx", symbol, vol/avg),
Detail: fmt.Sprintf("Price: $%.2f (24h: %.1f%%)", price, chg),
Price: price, Change: chg})
Price: price, Change: chg})
}
}
}
func (s *Sentinel) emit(sig Signal) {
s.logger.Info("signal", "type", sig.Type, "symbol", sig.Symbol, "title", sig.Title)
if s.onSignal != nil { s.onSignal(sig) }
if s.onSignal != nil {
s.onSignal(sig)
}
}