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:
jackyu66git
2026-05-03 18:05:19 +08:00
parent da561325d7
commit beb3611778
7 changed files with 528 additions and 316 deletions
+33 -82
View File
@@ -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 "??"
}