Files
exchange-monitor-go/scanner.go
T
jackyu66git e5ea78ffd2 feat: real trading mode, auto-stop after 5 trades, HL testnet support
- config.json: test_mode=false, ready for sim/testnet trading
- trader.go: auto-stop after 5 real trades, exchange response logging,
  Stop()/Start() API, shuttingDown flag for graceful stop
- dashboard.go: POST /api/stop + POST /api/start endpoints,
  trading status in SSE stats
- exchange/hyperliquid.go: switch HL WS to testnet endpoint
- exchange/hyperliquid_trade.go: switch REST to testnet endpoint,
  support base64 + 32-byte EVM private keys
- main.go: listen on trader.StopCh (graceful, no process exit)
- scanner.go: trim TrackedCoins to only 6 core coins (DOGE/LINK/ONDO/OP/WIF/ARB)
- .gitignore: ignore main binary
2026-05-04 14:31:52 +08:00

95 lines
2.9 KiB
Go

package main
import (
"sort"
)
// Exchange names — only Bitget and HyperLiquid are trading exchanges
const (
ExHyperLiquid = "HyperLiquid"
ExBitget = "Bitget"
)
// Taker fee rates (%) — for IOC market orders on trading exchanges
var takerFees = map[string]float64{
ExHyperLiquid: 0.045,
ExBitget: 0.060,
}
// 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 - 2*(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(snap map[string]map[string]float64) []*ArbOpportunity {
var results []*ArbOpportunity
for _, coin := range TrackedCoins {
exMap := snap[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, takerFees[ExBitget], takerFees[ExHyperLiquid])
// HL->BG: buy cheap at HyperLiquid, sell expensive at Bitget
profitHL := netProfit(hlP, bgP, takerFees[ExHyperLiquid], takerFees[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
}