package main import ( "log" "sort" "time" ) // Exchange names const ( ExBinance = "Binance" ExHyperLiquid = "HyperLiquid" ExBitget = "Bitget" ExDydx = "dYdX" ) // 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 } // 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, } // 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). // NOTE: Does NOT swap prices — callers (ScanArbWithFees) pass prices in explicit buy/sell order // and try both directions via addPair. Using exchange.CalcNetProfit would double-swap (B#6). 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] 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(ExHyperLiquid, ExBitget, hlP, bgP) addPair(ExHyperLiquid, ExDydx, hlP, dyP) addPair(ExBitget, ExDydx, bgP, dyP) 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" } return "??" }