P3-1: Scan optimization — only BG↔HL (50+ pair combos → 2)
P3-2: Real-time spread chart — spreadHistory ring buffer +
/api/spread-history endpoint + Chart.js spread chart
P3-3: Live position PnL — positions SSE now includes
estimated current profit/loss + current spread
P3-4: Real-time trade events — trader.OnTradeEvent callback
fires SSE 'trade_open' / 'trade_close' immediately
P3-5: Connection status monitoring — tracks last update time
per exchange, broadcast via stats.connections + /api/connections
Frontend: spread chart card, PnL column in positions,
connection status dots in stats bar,
green/red border flash on trade events
119 lines
3.6 KiB
Go
119 lines
3.6 KiB
Go
package main
|
|
|
|
import (
|
|
"sort"
|
|
)
|
|
|
|
// 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)
|
|
}
|
|
|
|
// ScanBGHL scans coins for arbitrage ONLY between Bitget and HyperLiquid (P3-1).
|
|
// Returns both directions (BG->HL and HL->BG) sorted by net profit descending.
|
|
func ScanBGHL(store *PriceStore) []*ArbOpportunity {
|
|
snapshot := store.GetAll()
|
|
var results []*ArbOpportunity
|
|
|
|
for _, coin := range TrackedCoins {
|
|
exMap := snapshot[coin.Name]
|
|
if exMap == nil {
|
|
continue
|
|
}
|
|
bgP := exMap[ExBitget]
|
|
hlP := exMap[ExHyperLiquid]
|
|
if bgP <= 0 || hlP <= 0 {
|
|
continue
|
|
}
|
|
|
|
// BG->HL: buy cheap at Bitget, sell expensive at HyperLiquid
|
|
profitBG := netProfit(bgP, hlP, makerFees[ExBitget], makerFees[ExHyperLiquid])
|
|
// HL->BG: buy cheap at HyperLiquid, sell expensive at Bitget
|
|
profitHL := netProfit(hlP, bgP, makerFees[ExHyperLiquid], makerFees[ExBitget])
|
|
|
|
grossBG := (hlP - bgP) / bgP * 100
|
|
grossHL := (bgP - hlP) / hlP * 100
|
|
|
|
results = append(results, &ArbOpportunity{
|
|
Coin: coin.Name,
|
|
Direction: "BG->HL",
|
|
BuyEx: ExBitget,
|
|
SellEx: ExHyperLiquid,
|
|
BuyPrice: bgP,
|
|
SellPrice: hlP,
|
|
NetProfit: profitBG,
|
|
GrossBasis: grossBG,
|
|
}, &ArbOpportunity{
|
|
Coin: coin.Name,
|
|
Direction: "HL->BG",
|
|
BuyEx: ExHyperLiquid,
|
|
SellEx: ExBitget,
|
|
BuyPrice: hlP,
|
|
SellPrice: bgP,
|
|
NetProfit: profitHL,
|
|
GrossBasis: grossHL,
|
|
})
|
|
}
|
|
|
|
sort.Slice(results, func(i, j int) bool {
|
|
return results[i].NetProfit > results[j].NetProfit
|
|
})
|
|
|
|
return results
|
|
}
|
|
|
|
// ScanArbWithFees checks all coins — REDIRECTED to ScanBGHL for performance (P3-1).
|
|
// Kept for backward compatibility; only BG↔HL is relevant for trading.
|
|
func ScanArbWithFees(store *PriceStore, fees map[string]float64) []*ArbOpportunity {
|
|
return ScanBGHL(store)
|
|
}
|
|
|
|
// ScanArb checks all coins using taker fees.
|
|
func ScanArb(store *PriceStore) []*ArbOpportunity {
|
|
return ScanArbWithFees(store, feeRates)
|
|
}
|