122 lines
2.9 KiB
Go
122 lines
2.9 KiB
Go
package main
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// TrackedCoin represents a coin we monitor across exchanges.
|
|
type TrackedCoin struct {
|
|
Name string // Display name (BTC, ETH, etc.)
|
|
BN string // Binance symbol (BTCUSDT)
|
|
BG string // Bitget symbol (BTCUSDT)
|
|
HL string // HyperLiquid symbol (BTC)
|
|
}
|
|
|
|
// PriceTick holds a price update with optional bid/ask.
|
|
type PriceTick struct {
|
|
Price float64
|
|
Bid float64 // 0 if unknown
|
|
Ask float64 // 0 if unknown
|
|
}
|
|
|
|
// Spread holds bid/ask data for one exchange+coin.
|
|
type Spread struct {
|
|
Bid float64
|
|
Ask float64
|
|
Updated int64 // unix nano
|
|
}
|
|
|
|
// PriceStore holds the latest prices from all exchanges, thread-safe.
|
|
type PriceStore struct {
|
|
mu sync.RWMutex
|
|
prices map[string]map[string]float64 // coin -> exchange -> price
|
|
spreads map[string]map[string]*Spread // coin -> exchange -> spread
|
|
}
|
|
|
|
func NewPriceStore() *PriceStore {
|
|
return &PriceStore{
|
|
prices: make(map[string]map[string]float64),
|
|
spreads: make(map[string]map[string]*Spread),
|
|
}
|
|
}
|
|
|
|
// Set stores a price update. If bid/ask are non-zero, also stores spread.
|
|
func (s *PriceStore) Set(coin, exchange string, price float64) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.prices[coin] == nil {
|
|
s.prices[coin] = make(map[string]float64)
|
|
}
|
|
s.prices[coin][exchange] = price
|
|
}
|
|
|
|
// SetWithSpread stores price + bid/ask spread.
|
|
func (s *PriceStore) SetWithSpread(coin, exchange string, price, bid, ask float64) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.prices[coin] == nil {
|
|
s.prices[coin] = make(map[string]float64)
|
|
}
|
|
s.prices[coin][exchange] = price
|
|
|
|
if bid > 0 && ask > 0 {
|
|
if s.spreads[coin] == nil {
|
|
s.spreads[coin] = make(map[string]*Spread)
|
|
}
|
|
s.spreads[coin][exchange] = &Spread{
|
|
Bid: bid, Ask: ask,
|
|
Updated: time.Now().UnixNano(),
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *PriceStore) Get(coin, exchange string) (float64, bool) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
p, ok := s.prices[coin][exchange]
|
|
return p, ok
|
|
}
|
|
|
|
// GetSpread returns the current bid-ask spread (as percentage of mid price).
|
|
// Returns 0 if no spread data available.
|
|
func (s *PriceStore) GetSpread(coin, exchange string) float64 {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
sp, ok := s.spreads[coin][exchange]
|
|
if !ok || sp.Bid <= 0 || sp.Ask <= 0 {
|
|
return 0
|
|
}
|
|
mid := (sp.Bid + sp.Ask) / 2
|
|
if mid <= 0 {
|
|
return 0
|
|
}
|
|
return (sp.Ask - sp.Bid) / mid * 100
|
|
}
|
|
|
|
// GetAll returns a snapshot of all prices.
|
|
func (s *PriceStore) GetAll() map[string]map[string]float64 {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
snap := make(map[string]map[string]float64)
|
|
for coin, exMap := range s.prices {
|
|
snap[coin] = make(map[string]float64)
|
|
for ex, p := range exMap {
|
|
snap[coin][ex] = p
|
|
}
|
|
}
|
|
return snap
|
|
}
|
|
|
|
// ArbOpportunity represents a profitable arbitrage route.
|
|
type ArbOpportunity struct {
|
|
Coin string
|
|
Direction string // e.g. "BN->HL"
|
|
BuyEx string
|
|
SellEx string
|
|
BuyPrice float64
|
|
SellPrice float64
|
|
NetProfit float64 // percentage after fees
|
|
GrossBasis float64 // raw price difference %
|
|
}
|