Files
exchange-monitor-go/scanner.go
T
2026-05-03 16:54:36 +08:00

176 lines
4.4 KiB
Go

package main
import (
"log"
"sort"
"time"
)
// Exchange names
const (
ExBinance = "Binance"
ExHyperLiquid = "HyperLiquid"
ExBitget = "Bitget"
ExDydx = "dYdX"
ExAevo = "Aevo"
)
// Fee rates (%) — taker fees per exchange
var feeRates = map[string]float64{
ExBinance: 0.040,
ExHyperLiquid: 0.035,
ExBitget: 0.040, // standard taker
ExDydx: 0.050, // dYdX v4 standard taker
ExAevo: 0.050, // Aevo standard taker
}
// Maker fee rates (%) — for limit orders
var makerFees = map[string]float64{
ExBinance: 0.020, // standard maker (USDT pairs)
ExHyperLiquid: 0.015,
ExBitget: 0.020, // standard maker
ExDydx: 0.020,
ExAevo: 0.020,
}
// TickerCoins defines all coins we monitor.
var TrackedCoins = []TrackedCoin{
{Name: "DOGE", BN: "DOGEUSDT", BG: "DOGEUSDT", HL: "DOGE"},
{Name: "LINK", BN: "LINKUSDT", BG: "LINKUSDT", HL: "LINK"},
{Name: "ONDO", BN: "ONDOUSDT", BG: "ONDOUSDT", HL: "ONDO"},
{Name: "OP", BN: "OPUSDT", BG: "OPUSDT", HL: "OP"},
{Name: "WIF", BN: "WIFUSDT", BG: "WIFUSDT", HL: "WIF"},
{Name: "ARB", BN: "ARBUSDT", BG: "ARBUSDT", HL: "ARB"},
}
// netProfit calculates net profit % after fees for a complete round trip (entry + exit).
func netProfit(buyPrice, sellPrice, buyFee, sellFee float64) float64 {
if buyPrice <= 0 || sellPrice <= 0 {
return 0
}
// Entry: buy at buyPrice (pay buyFee), sell short at sellPrice (pay sellFee)
cost := buyPrice * (1 + buyFee/100)
revenue := sellPrice * (1 - sellFee/100)
// Exit: sell long (pay sellFee), buy back short (pay buyFee)
// Total fees = 2 * (buyFee + sellFee), first round already in formula above
return (revenue/cost - 1)*100 - (buyFee + sellFee)
}
// ScanArbWithFees checks all coins for arbitrage opportunities using a custom fee map.
// Pass feeRates for taker fees or makerFees for limit order fees.
func ScanArbWithFees(store *PriceStore, fees map[string]float64) []*ArbOpportunity {
snapshot := store.GetAll()
var results []*ArbOpportunity
for _, coin := range TrackedCoins {
coinStart := time.Now()
exMap := snapshot[coin.Name]
if exMap == nil {
continue
}
bnP := exMap[ExBinance]
hlP := exMap[ExHyperLiquid]
bgP := exMap[ExBitget]
dyP := exMap[ExDydx]
aeP := exMap[ExAevo]
var pairs []struct {
profit float64
buyEx string
sellEx string
buyP float64
sellP float64
}
addPair := func(ex1, ex2 string, p1, p2 float64) {
if p1 <= 0 || p2 <= 0 {
return
}
pairs = append(pairs,
struct {
profit float64
buyEx string
sellEx string
buyP float64
sellP float64
}{netProfit(p1, p2, fees[ex1], fees[ex2]), ex1, ex2, p1, p2},
struct {
profit float64
buyEx string
sellEx string
buyP float64
sellP float64
}{netProfit(p2, p1, fees[ex2], fees[ex1]), ex2, ex1, p2, p1},
)
}
addPair(ExBinance, ExHyperLiquid, bnP, hlP)
addPair(ExBinance, ExBitget, bnP, bgP)
addPair(ExBinance, ExDydx, bnP, dyP)
addPair(ExBinance, ExAevo, bnP, aeP)
addPair(ExHyperLiquid, ExBitget, hlP, bgP)
addPair(ExHyperLiquid, ExDydx, hlP, dyP)
addPair(ExHyperLiquid, ExAevo, hlP, aeP)
addPair(ExBitget, ExDydx, bgP, dyP)
addPair(ExBitget, ExAevo, bgP, aeP)
addPair(ExDydx, ExAevo, dyP, aeP)
if len(pairs) == 0 {
continue
}
best := pairs[0]
for _, p := range pairs[1:] {
if p.profit > best.profit {
best = p
}
}
grossBasis := (best.sellP - best.buyP) / best.buyP * 100
results = append(results, &ArbOpportunity{
Coin: coin.Name,
Direction: shortName(best.buyEx) + "->" + shortName(best.sellEx),
BuyEx: best.buyEx,
SellEx: best.sellEx,
BuyPrice: best.buyP,
SellPrice: best.sellP,
NetProfit: best.profit,
GrossBasis: grossBasis,
})
coinElapsed := time.Since(coinStart)
if coinElapsed > time.Millisecond {
log.Printf("[Profile] scan %s took %dµs", coin.Name, coinElapsed.Microseconds())
}
}
sort.Slice(results, func(i, j int) bool {
return results[i].NetProfit > results[j].NetProfit
})
return results
}
// ScanArb checks all coins using taker fees.
func ScanArb(store *PriceStore) []*ArbOpportunity {
return ScanArbWithFees(store, feeRates)
}
func shortName(exchange string) string {
switch exchange {
case ExBinance:
return "BN"
case ExHyperLiquid:
return "HL"
case ExBitget:
return "BG"
case ExDydx:
return "dYdX"
case ExAevo:
return "Ae"
}
return "??"
}