Find top traders and read their record
Rank Hyperliquid traders by 30-day realized PnL, then pull the best one's trade statistics, with the median beside the mean.
Two calls. /traders ranks the venue; the first row's address goes
to /wallets/{address}/trades/summary, which
returns that wallet's statistics overall and per coin.
export VIKE_API_KEY=...
python hl-top-traders.py # real run: needs the key
python hl-top-traders.py --dry-run # prints the requests it would make, calls nothingThe script prints the three leaders, then the winner's trade count, net PnL and the mean and median of its gains and losses. A wallet with no trades in the window ends the run with a message naming the recipe.
"""Rank Hyperliquid traders by 30-day realized PnL, then look inside the best one's record.
Run: VIKE_API_KEY=... python hl-top-traders.py (--dry-run lists the requests and makes none)
"""
import os
import sys
from urllib.parse import urlencode
NAME = "hl-top-traders"
BASE = "https://data.vike.io"
DRY = "--dry-run" in sys.argv
def fail(why):
sys.exit(f"{NAME}: {why}")
def call(path, method="GET", **params):
url = BASE + path + ("?" + urlencode(params) if params else "")
if DRY:
print(method, url)
return None
key = os.environ.get("VIKE_API_KEY") or fail("set VIKE_API_KEY")
import requests
r = requests.request(method, url, headers={"X-API-Key": key}, timeout=60)
if r.status_code != 200:
fail(f"{method} {path} answered HTTP {r.status_code}: {r.text[:200]}")
return r.json()
def main():
# /traders returns one page of at most 20 and has no cursor; three is plenty here.
board = call("/v1/hyperliquid/traders", window="30d", sort="pnl", size=3)
wallet = "0x25773676902085225ae5751075e4161369334897" # --dry-run has no response to read
if not DRY:
if not board["traders"]:
fail("/traders returned no traders")
for t in board["traders"]:
print(f"{t['address']} pnl ${t['total_realized_pnl']:,.0f} win rate {t['win_rate_pct']}% trades {t['trade_count']}")
wallet = board["traders"][0]["address"]
summary = call(f"/v1/hyperliquid/wallets/{wallet}/trades/summary", window="30d")
if DRY:
return
o = summary["overall"]
if not o["trade_count"]:
fail(f"{wallet} has no trades in the window")
# One outsized trade drags a mean far from the typical trade, so read the median beside it.
print(f"{wallet}: {o['trade_count']} trades, net ${o['net_pnl']:,.0f}")
print(f" gain mean ${o['avg_gain']:,.2f} median ${o['median_gain']:,.2f}")
print(f" loss mean ${o['avg_loss']:,.2f} median ${o['median_loss']:,.2f}")
if __name__ == "__main__":
try:
main()
except Exception as e: # a shape the recipe does not expect fails by name, not by traceback
fail(f"{type(e).__name__}: {e}")Read it this way
- Mean against median. The summary carries both because one outsized trade drags a mean far from anything
the wallet typically does. If
avg_gainis many timesmedian_gain, a few trades made the record. - Pass the window to both calls. Both take
1d,7d,30dorall, but their defaults differ:/tradersranks over30dwhen you send nothing and the summary coversall. Leave the second window out and the leaderboard's 30 days become the wallet's whole history. - Even with the same window the two counts differ.
/traderscounts completed trades by the day they opened; the summary also counts open trades and trades opened before the window, so itstrade_countandnet_pnlrun higher. The/traderspage reconciles one wallet.
Three behaviours of /traders that a script trips over
size is clamped to 20 whatever you send, there is no cursor, and an unrecognised sort silently falls back to
the PnL ranking rather than failing. For a deeper board, with a cursor and a 400 on a typo, use
/leaderboard.