misc: binance momentum, telegram, frontend updates

This commit is contained in:
jackyu66git
2026-06-03 09:39:54 +08:00
parent b2ff322ef3
commit 23f3c61c9f
20 changed files with 928 additions and 86 deletions
+220
View File
@@ -0,0 +1,220 @@
package main
import (
"math"
"sync"
"time"
)
// bmBuffer is a per-coin ring buffer for Binance prices.
type bmBuffer struct {
prices []float64
head int
count int
}
func newBmBuffer(capacity int) *bmBuffer {
return &bmBuffer{
prices: make([]float64, capacity),
}
}
func (b *bmBuffer) push(price float64) {
b.prices[b.head] = price
b.head = (b.head + 1) % len(b.prices)
if b.count < len(b.prices) {
b.count++
}
}
func (b *bmBuffer) isFull() bool {
return b.count == len(b.prices)
}
// oldest returns the price written exactly `capacity` ticks ago.
func (b *bmBuffer) oldest() float64 {
if !b.isFull() {
return 0
}
return b.prices[b.head]
}
// newest returns the most recently written price.
func (b *bmBuffer) newest() float64 {
if b.count == 0 {
return 0
}
idx := b.head - 1
if idx < 0 {
idx = len(b.prices) - 1
}
return b.prices[idx]
}
// BinanceAlert is emitted when a coin's 1-minute Binance change exceeds threshold.
type BinanceAlert struct {
Coin string `json:"coin"`
Price float64 `json:"price"`
OldPrice float64 `json:"old_price"`
ChangePct float64 `json:"change_pct"`
Direction string `json:"direction"`
ThresholdPct float64 `json:"threshold_pct"`
Timestamp time.Time `json:"timestamp"`
}
// BinanceMomentumSnapshot is the current state of a coin for SSE push.
type BinanceMomentumSnapshot struct {
Coin string `json:"coin"`
Price float64 `json:"price"`
ChangePct float64 `json:"change_pct"`
Direction string `json:"direction"`
BufferFull bool `json:"buffer_full"`
}
// BinanceMomentumDetector tracks Binance price changes over a configurable window.
type BinanceMomentumDetector struct {
mu sync.Mutex
windowTicks int
thresholdPct float64
cooldownSec int
buffers map[string]*bmBuffer
lastAlertAt map[string]time.Time
recentAlerts []BinanceAlert
maxAlerts int
}
// NewBinanceMomentumDetector creates a detector for all TrackedCoins.
func NewBinanceMomentumDetector(windowSec int, tickMs int, thresholdPct float64, cooldownSec int) *BinanceMomentumDetector {
windowTicks := windowSec * 1000 / tickMs
if windowTicks < 1 {
windowTicks = 1
}
d := &BinanceMomentumDetector{
windowTicks: windowTicks,
thresholdPct: thresholdPct,
cooldownSec: cooldownSec,
buffers: make(map[string]*bmBuffer, len(TrackedCoins)),
lastAlertAt: make(map[string]time.Time),
maxAlerts: 200,
}
for _, tc := range TrackedCoins {
d.buffers[tc.Name] = newBmBuffer(windowTicks)
}
return d
}
// Record appends the current Binance price for a coin into its ring buffer.
func (d *BinanceMomentumDetector) Record(coin string, price float64) {
d.mu.Lock()
buf, ok := d.buffers[coin]
if ok {
buf.push(price)
}
d.mu.Unlock()
}
// Detect checks all coins for threshold breaches. Returns new alerts.
func (d *BinanceMomentumDetector) Detect() []BinanceAlert {
d.mu.Lock()
defer d.mu.Unlock()
now := time.Now()
var alerts []BinanceAlert
for coin, buf := range d.buffers {
if !buf.isFull() {
continue
}
old := buf.oldest()
current := buf.newest()
if old <= 0 || current <= 0 {
continue
}
changePct := (current - old) / old * 100
if math.Abs(changePct) < d.thresholdPct {
continue
}
lastAt, exists := d.lastAlertAt[coin]
if exists && now.Sub(lastAt).Seconds() < float64(d.cooldownSec) {
continue
}
dir := "up"
if changePct < 0 {
dir = "down"
}
alert := BinanceAlert{
Coin: coin,
Price: current,
OldPrice: old,
ChangePct: changePct,
Direction: dir,
ThresholdPct: d.thresholdPct,
Timestamp: now,
}
alerts = append(alerts, alert)
d.lastAlertAt[coin] = now
// Store in recent alerts ring buffer
d.recentAlerts = append(d.recentAlerts, alert)
if len(d.recentAlerts) > d.maxAlerts {
d.recentAlerts = d.recentAlerts[len(d.recentAlerts)-d.maxAlerts:]
}
}
return alerts
}
// Snapshot returns the current momentum state for all coins.
func (d *BinanceMomentumDetector) Snapshot() []BinanceMomentumSnapshot {
d.mu.Lock()
defer d.mu.Unlock()
snapshots := make([]BinanceMomentumSnapshot, 0, len(d.buffers))
for coin, buf := range d.buffers {
s := BinanceMomentumSnapshot{
Coin: coin,
BufferFull: buf.isFull(),
}
if buf.isFull() {
old := buf.oldest()
current := buf.newest()
s.Price = current
if old > 0 {
s.ChangePct = (current - old) / old * 100
if s.ChangePct >= 0 {
s.Direction = "up"
} else {
s.Direction = "down"
}
}
} else if buf.count > 0 {
s.Price = buf.newest()
s.Direction = "flat"
}
snapshots = append(snapshots, s)
}
return snapshots
}
// RecentAlerts returns the last N alerts.
func (d *BinanceMomentumDetector) RecentAlerts(limit int) []BinanceAlert {
d.mu.Lock()
defer d.mu.Unlock()
if limit <= 0 || limit > len(d.recentAlerts) {
limit = len(d.recentAlerts)
}
if limit == 0 {
return nil
}
start := len(d.recentAlerts) - limit
r := make([]BinanceAlert, limit)
copy(r, d.recentAlerts[start:])
// Reverse so newest is first
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return r
}