fix: fetch all Hyperliquid fills — UserFillsByTime is capped at 100

Root cause of the dashboard over-reporting profit while the account lost
money: the fill sync used UserFillsByTime, which hard-caps at 100 fills per
response. At 20x/4-position frequency the account does >100 fills/24h, so
~20% of fills were silently dropped — and the dropped ones skewed toward
losers, inflating recorded PnL and under-counting fees. Verified: over a 46h
window the DB showed net +$39 while Hyperliquid official was -$18.6, and the
equity drop ($176 -> $156) confirmed the loss.

Switch GetTrades to UserFills (returns up to 2000 recent fills), filtering to
startTime client-side, and widen the sync lookback 24h -> 7d so gaps
backfill. Verified live: a sync now pulls 675 fills where it previously
always received exactly 100.

Note: this stops future drift; already-corrupted historical position rows are
not retroactively rebuilt (dedup blocks re-processing). Account equity remains
the authoritative scoreboard.
This commit is contained in:
tinkle-community
2026-07-16 11:11:56 +09:00
parent eabd279d10
commit 0f3e71560c
2 changed files with 18 additions and 5 deletions

View File

@@ -20,13 +20,16 @@ func (t *HyperliquidTrader) SyncOrdersFromHyperliquid(traderID string, exchangeI
return fmt.Errorf("store is nil") return fmt.Errorf("store is nil")
} }
// Get recent trades (last 24 hours) // Look back 7 days. GetTrades now pulls up to 2000 recent fills (UserFills)
startTime := time.Now().Add(-24 * time.Hour) // and filters to this window, so a wide lookback backfills any fills missed
// during past outages/gaps without dropping recent ones. Dedup by trade ID
// keeps re-processing idempotent.
startTime := time.Now().Add(-7 * 24 * time.Hour)
logger.Infof("🔄 Syncing Hyperliquid trades from: %s", startTime.Format(time.RFC3339)) logger.Infof("🔄 Syncing Hyperliquid trades from: %s", startTime.Format(time.RFC3339))
// Use GetTrades method to fetch trade records // Use GetTrades method to fetch trade records
trades, err := t.GetTrades(startTime, 1000) trades, err := t.GetTrades(startTime, 2000)
if err != nil { if err != nil {
return fmt.Errorf("failed to get trades: %w", err) return fmt.Errorf("failed to get trades: %w", err)
} }

View File

@@ -11,6 +11,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
hl "github.com/sonirico/go-hyperliquid"
) )
// GetBalance gets account balance // GetBalance gets account balance
@@ -548,15 +550,23 @@ func (t *HyperliquidTrader) GetClosedPnL(startTime time.Time, limit int) ([]type
// GetTrades retrieves trade history from Hyperliquid // GetTrades retrieves trade history from Hyperliquid
func (t *HyperliquidTrader) GetTrades(startTime time.Time, limit int) ([]types.TradeRecord, error) { func (t *HyperliquidTrader) GetTrades(startTime time.Time, limit int) ([]types.TradeRecord, error) {
// Use UserFillsByTime API // Use UserFills (returns up to 2000 recent fills) rather than
// UserFillsByTime, which is hard-capped at 100 fills per response. At
// this trading frequency the account exceeds 100 fills/24h, so
// UserFillsByTime silently dropped ~20% of fills — skewing recorded PnL
// and fees away from the exchange truth. 2000 recent fills covers many
// days of history; we filter to startTime client-side.
startTimeMs := startTime.UnixMilli() startTimeMs := startTime.UnixMilli()
fills, err := t.exchange.Info().UserFillsByTime(t.ctx, t.walletAddr, startTimeMs, nil, nil) fills, err := t.exchange.Info().UserFills(t.ctx, hl.UserFillsParams{Address: t.walletAddr})
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get user fills: %w", err) return nil, fmt.Errorf("failed to get user fills: %w", err)
} }
var trades []types.TradeRecord var trades []types.TradeRecord
for _, fill := range fills { for _, fill := range fills {
if fill.Time < startTimeMs {
continue
}
price, _ := strconv.ParseFloat(fill.Price, 64) price, _ := strconv.ParseFloat(fill.Price, 64)
qty, _ := strconv.ParseFloat(fill.Size, 64) qty, _ := strconv.ParseFloat(fill.Size, 64)
fee, _ := strconv.ParseFloat(fill.Fee, 64) fee, _ := strconv.ParseFloat(fill.Fee, 64)