misc: binance momentum, telegram, frontend updates
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user