Phase 3: Real-time enhancements
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
This commit is contained in:
+33
-82
@@ -1,9 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Exchange names
|
||||
@@ -55,88 +53,50 @@ func netProfit(buyPrice, sellPrice, buyFee, sellFee float64) float64 {
|
||||
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 {
|
||||
// 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 {
|
||||
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 {
|
||||
hlP := exMap[ExHyperLiquid]
|
||||
if bgP <= 0 || hlP <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
best := pairs[0]
|
||||
for _, p := range pairs[1:] {
|
||||
if p.profit > best.profit {
|
||||
best = p
|
||||
}
|
||||
}
|
||||
// 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])
|
||||
|
||||
grossBasis := (best.sellP - best.buyP) / best.buyP * 100
|
||||
grossBG := (hlP - bgP) / bgP * 100
|
||||
grossHL := (bgP - hlP) / hlP * 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,
|
||||
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,
|
||||
})
|
||||
|
||||
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 {
|
||||
@@ -146,22 +106,13 @@ func ScanArbWithFees(store *PriceStore, fees map[string]float64) []*ArbOpportuni
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
func shortName(exchange string) string {
|
||||
switch exchange {
|
||||
case ExBinance:
|
||||
return "BN"
|
||||
case ExHyperLiquid:
|
||||
return "HL"
|
||||
case ExBitget:
|
||||
return "BG"
|
||||
case ExDydx:
|
||||
return "dYdX"
|
||||
}
|
||||
return "??"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user