Skip to content

Commit 78842d6

Browse files
committed
fixed Leaderboard APIs and added + examples
1 parent b653c90 commit 78842d6

File tree

11 files changed

+363
-150
lines changed

11 files changed

+363
-150
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hyperliquid-ts",
3-
"version": "1.0.1",
3+
"version": "1.1.1",
44
"description": "Unofficial TypeScript SDK for Hyperliquid 🍧",
55
"main": "dist/index.js",
66
"module": "dist/index.js",
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { HyperliquidAPI } from '../../src';
2+
import type { LeaderboardFilter, TimeWindow, TraderPosition, LeaderboardEntry } from '../../src';
3+
import { t } from 'tasai';
4+
5+
const highlight = t.bold.cyan.toFunction();
6+
const header = t.bold.underline.magenta.toFunction();
7+
const subHeader = t.bold.yellow.toFunction();
8+
const positive = t.green.toFunction();
9+
const negative = t.red.toFunction();
10+
const value = t.bold.white.toFunction();
11+
12+
async function runTraderAnalysis() {
13+
const api = new HyperliquidAPI();
14+
15+
const filter: LeaderboardFilter = {
16+
timeWindow: 'month' as TimeWindow,
17+
minAccountValue: 100_000,
18+
minVolume: 1_000_000,
19+
maxVolume: 100_000_000,
20+
minPnL: 10_000,
21+
minRoi: 0.5,
22+
maxAccounts: 3
23+
};
24+
25+
try {
26+
console.log(header("Fetching and analyzing top traders..."));
27+
const analysis = await analyzeTradersData(api, filter);
28+
29+
console.log(header("\nTop Traders:"));
30+
analysis.topTraders.forEach((trader, index) => {
31+
console.log(subHeader(`\n#${index + 1}:`));
32+
console.log(`Address: ${highlight(trader.ethAddress)}`);
33+
console.log(`Account Value: ${value('$' + formatNumber(trader.accountValue))}`);
34+
console.log(`PnL: ${formatPnL(trader.windowPerformances[0][1].pnl)}`);
35+
console.log(`ROI: ${formatPercentage(trader.windowPerformances[0][1].roi)}`);
36+
console.log(`Volume: ${value('$' + formatNumber(trader.windowPerformances[0][1].vlm))}`);
37+
console.log(`Open Positions: ${value(trader.totalOpenPositions)}`);
38+
console.log(`Trade Count: ${value(trader.tradeCount)}`);
39+
});
40+
41+
console.log(header("\nOverall Analysis:"));
42+
console.log(`Shared Assets: ${highlight(analysis.analysis.sharedAssets.join(', '))}`);
43+
console.log(`Overall Sentiment: ${formatSentiment(analysis.analysis.overallSentiment)}`);
44+
console.log(`Risk Analysis: ${value(analysis.analysis.riskAnalysis)}`);
45+
console.log(`Trading Activity: ${value(analysis.analysis.tradingActivity)}`);
46+
47+
} catch (error) {
48+
console.error(negative("Error running trader analysis:"), error);
49+
} finally {
50+
api.disconnect();
51+
}
52+
}
53+
54+
async function analyzeTradersData(api: HyperliquidAPI, filter: LeaderboardFilter, sortBy: 'pnl' | 'roi' | 'vlm' | 'accountValue' = 'pnl') {
55+
const leaderboard = await api.leaderboard.getLeaderboard();
56+
const filteredLeaderboard = await api.leaderboard.filterLeaderboard(leaderboard, filter);
57+
const sortedLeaderboard = api.leaderboard.sortLeaderboard(filteredLeaderboard, sortBy, filter.timeWindow);
58+
const topTraders = sortedLeaderboard.slice(0, filter.maxAccounts);
59+
60+
const extendedTraderInfo = await Promise.all(
61+
topTraders.map(trader => getExtendedTraderInfo(api, trader, filter.timeWindow || 'allTime'))
62+
);
63+
64+
return {
65+
topTraders: extendedTraderInfo,
66+
analysis: {
67+
sharedAssets: findSharedAssets(extendedTraderInfo.map(info => info.openPositions)),
68+
overallSentiment: calculateOverallSentiment(extendedTraderInfo.map(info => info.openPositions)),
69+
riskAnalysis: analyzeRisk(extendedTraderInfo.map(info => info.openPositions)),
70+
tradingActivity: analyzeTradingActivity(extendedTraderInfo),
71+
},
72+
};
73+
}
74+
75+
async function getExtendedTraderInfo(api: HyperliquidAPI, trader: LeaderboardEntry, timeWindow: TimeWindow): Promise<any> {
76+
const openPositions = await api.leaderboard.getTraderOpenPositions(trader.ethAddress);
77+
const tradeCount = await api.leaderboard.getTraderTradeCount(trader.ethAddress, getStartTimeForWindow(timeWindow), Date.now());
78+
79+
return {
80+
...trader,
81+
openPositions,
82+
totalOpenPositions: openPositions.perp.length + openPositions.spot.length,
83+
tradeCount: tradeCount.total,
84+
};
85+
}
86+
87+
function findSharedAssets(positions: Array<{ perp: TraderPosition[]; spot: TraderPosition[] }>): string[] {
88+
const assetCounts: Record<string, number> = {};
89+
positions.forEach(traderPositions => {
90+
[...traderPositions.perp, ...traderPositions.spot].forEach(position => {
91+
assetCounts[position.asset] = (assetCounts[position.asset] || 0) + 1;
92+
});
93+
});
94+
return Object.entries(assetCounts)
95+
.filter(([_, count]) => count > 1)
96+
.map(([asset, _]) => asset);
97+
}
98+
99+
function calculateOverallSentiment(
100+
positions: Array<{ perp: TraderPosition[]; spot: TraderPosition[] }>
101+
): 'bullish' | 'bearish' | 'neutral' {
102+
let totalSentiment = 0;
103+
let positionCount = 0;
104+
105+
positions.forEach(traderPositions => {
106+
traderPositions.perp.forEach(position => {
107+
totalSentiment += Math.sign(position.size);
108+
positionCount++;
109+
});
110+
});
111+
112+
const averageSentiment = totalSentiment / positionCount;
113+
if (averageSentiment > 0.2) return 'bullish';
114+
if (averageSentiment < -0.2) return 'bearish';
115+
return 'neutral';
116+
}
117+
118+
function analyzeRisk(positions: Array<{ perp: TraderPosition[]; spot: TraderPosition[] }>): string {
119+
let highLeverageCount = 0;
120+
let totalPositions = 0;
121+
122+
positions.forEach(traderPositions => {
123+
traderPositions.perp.forEach(position => {
124+
if (position.leverage > 10) highLeverageCount++;
125+
totalPositions++;
126+
});
127+
});
128+
129+
const highLeverageRatio = highLeverageCount / totalPositions;
130+
if (highLeverageRatio > 0.5) return 'High risk: Many positions use high leverage';
131+
if (highLeverageRatio > 0.2) return 'Moderate risk: Some positions use high leverage';
132+
return 'Low risk: Most positions use conservative leverage';
133+
}
134+
135+
function analyzeTradingActivity(traders: any[]): string {
136+
const avgTradeCount = traders.reduce((sum, trader) => sum + trader.tradeCount, 0) / traders.length;
137+
const avgOpenPositions = traders.reduce((sum, trader) => sum + trader.totalOpenPositions, 0) / traders.length;
138+
139+
return `Average trade count: ${avgTradeCount.toFixed(2)}. Average open positions: ${avgOpenPositions.toFixed(2)}.`;
140+
}
141+
142+
function getStartTimeForWindow(timeWindow: TimeWindow): number {
143+
const now = Date.now();
144+
switch (timeWindow) {
145+
case 'day': return now - 24 * 60 * 60 * 1000;
146+
case 'week': return now - 7 * 24 * 60 * 60 * 1000;
147+
case 'month': return now - 30 * 24 * 60 * 60 * 1000;
148+
default: return 0; // For 'allTime', return 0 to get all trades
149+
}
150+
}
151+
152+
function formatNumber(value: string | number, decimals: number = 2): string {
153+
return parseFloat(value.toString()).toLocaleString('en-US', { maximumFractionDigits: decimals });
154+
}
155+
156+
function formatPercentage(value: string | number): string {
157+
const percentage = (parseFloat(value.toString()) * 100).toFixed(2);
158+
return percentage.startsWith('-') ? negative(`${percentage}%`) : positive(`${percentage}%`);
159+
}
160+
161+
function formatPnL(value: string | number): string {
162+
const formatted = `$${formatNumber(value)}`;
163+
return parseFloat(value.toString()) >= 0 ? positive(formatted) : negative(formatted);
164+
}
165+
166+
function formatSentiment(sentiment: 'bullish' | 'bearish' | 'neutral'): string {
167+
switch (sentiment) {
168+
case 'bullish': return positive(sentiment);
169+
case 'bearish': return negative(sentiment);
170+
default: return value(sentiment);
171+
}
172+
}
173+
174+
runTraderAnalysis().then(() => {
175+
console.log(highlight("\nAnalysis complete. Exiting..."));
176+
process.exit(0);
177+
}).catch(error => {
178+
console.error(negative("Unhandled error:"), error);
179+
process.exit(1);
180+
});

scripts/examples/leaderboard.example.ts renamed to scripts/examples/topTradersPositions.ts

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { HyperliquidAPI } from "../../src";
2-
import type { LeaderboardFilter, TimeWindow, TraderPosition } from "../../src";
1+
import {HyperliquidAPI} from "../../src";
2+
import type { BestTrade, LeaderboardFilter, TimeWindow, TraderPosition, UserFill } from "../../src";
33
import {Color, t} from "tasai";
44
const highlight = t.bold.cyan.toFunction();
55
const header = t.bold.underline.magenta.toFunction();
@@ -28,6 +28,8 @@ async function runLeaderboardAnalysis() {
2828
minPnL: 10_000,
2929
minRoi: 0.5,
3030
maxAccounts: 3
31+
// This adds majorly to HL API usage
32+
//maxTrades: 1000
3133
};
3234

3335
try {
@@ -74,15 +76,26 @@ async function runLeaderboardAnalysis() {
7476
}
7577

7678
// Fetch and display best trade
77-
const bestTrade = await getBestTrade(api, trader.ethAddress, filter.timeWindow!);
79+
const bestTrade = await api.leaderboard.getBestTrade(trader.ethAddress, filter.timeWindow!);
7880
if (bestTrade) {
7981
console.log(subHeader("\nBest Trade:"));
8082
console.log(` Asset: ${ticker(bestTrade.coin)}`);
83+
console.log(` Market: ${value(bestTrade.isPerp ? 'Perpetual' : 'Spot')}`);
8184
console.log(` Side: ${bestTrade.side === 'buy' ? positive(bestTrade.side) : negative(bestTrade.side)}`);
8285
console.log(` Price: ${value('$' + formatNumber(bestTrade.px))}`);
8386
console.log(` Size: ${value(formatNumber(bestTrade.sz, 4))}`);
87+
if (bestTrade.isPerp && bestTrade.leverage !== 1) {
88+
const levColor = leverageColor(bestTrade.leverage!);
89+
console.log(` Leverage: ${levColor(bestTrade.leverage + 'x')}`);
90+
}
8491
console.log(` PnL: ${formatPnL(bestTrade.closedPnl)}`);
8592
console.log(` Time: ${new Date(bestTrade.time).toLocaleString()}`);
93+
if (bestTrade.liquidation) {
94+
console.log(highlight(" Liquidation:"));
95+
console.log(` Liquidated User: ${value(bestTrade.liquidation.liquidatedUser)}`);
96+
console.log(` Mark Price: ${value('$' + formatNumber(bestTrade.liquidation.markPx))}`);
97+
console.log(` Method: ${value(bestTrade.liquidation.method)}`);
98+
}
8699
} else {
87100
console.log(negative("\nNo trades found for this period"));
88101
}
@@ -96,38 +109,16 @@ async function runLeaderboardAnalysis() {
96109
}
97110

98111
function displayPosition(position: TraderPosition) {
99-
console.log(` ${ticker(position.asset)}: ${value(formatNumber(position.size, 4))} @ $${value(formatNumber(position.entryPrice))}`);
100-
if (position.unrealizedPnl) {
112+
console.log(` ${ticker(position.asset)}: ${value(formatNumber(position.size, 4))} ${position.entryPrice ? `@ $${value(formatNumber(position.entryPrice))}` : ''}`);
113+
if (position.unrealizedPnl !== null) {
101114
console.log(` Unrealized PnL: ${formatPnL(position.unrealizedPnl.toString())}`);
102115
}
103-
if (position.leverage && position.leverage !== 1) {
116+
if (position.leverage !== 1) {
104117
const leverageStyled = leverageColor(position.leverage)(`${position.leverage}x`);
105118
console.log(` Leverage: ${leverageStyled}`);
106119
}
107120
}
108121

109-
async function getBestTrade(api: HyperliquidAPI, trader: string, timeWindow: TimeWindow) {
110-
const endTime = Date.now();
111-
const startTime = getStartTimeForWindow(timeWindow);
112-
const trades = await api.info.getUserFillsByTime(trader, startTime, endTime);
113-
return trades.reduce((best, current) => {
114-
if (!best || parseFloat(current.closedPnl) > parseFloat(best.closedPnl)) {
115-
return current;
116-
}
117-
return best;
118-
}, null as any);
119-
}
120-
121-
function getStartTimeForWindow(timeWindow: TimeWindow): number {
122-
const now = Date.now();
123-
switch (timeWindow) {
124-
case 'day': return now - 24 * 60 * 60 * 1000;
125-
case 'week': return now - 7 * 24 * 60 * 60 * 1000;
126-
case 'month': return now - 30 * 24 * 60 * 60 * 1000;
127-
default: return 0; // For 'allTime', return 0 to get all trades
128-
}
129-
}
130-
131122
function formatNumber(value: string | number | undefined, decimals: number = 2): string {
132123
return parseFloat(value?.toString() || '0').toLocaleString('en-US', {maximumFractionDigits: decimals});
133124
}

src/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export class HyperliquidAPI {
132132

133133
this.exchange = this.createAuthenticatedProxy(ExchangeAPI);
134134
this.custom = this.createAuthenticatedProxy(CustomOperations);
135-
this.leaderboard = new LeaderboardAPI(this.httpApi);
135+
this.leaderboard = new LeaderboardAPI(this.httpApi, this.info.generalAPI, this.info.perpetuals, this.info.spot);
136136

137137
if (privateKey) {
138138
this.initializeWithPrivateKey(privateKey, baseURL);

src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export const INFO_TYPES = {
2121
USER_FILLS: 'userFills',
2222
USER_FILLS_BY_TIME: 'userFillsByTime',
2323
USER_RATE_LIMIT: 'userRateLimit',
24+
SPOT_USER_FILLS_BY_TIME: 'spotUserFillsByTime' as const,
25+
TRADE_INFO: 'tradeInfo' as const,
2426
ORDER_STATUS: 'orderStatus',
2527
L2_BOOK: 'l2Book',
2628
CANDLE_SNAPSHOT: 'candleSnapshot',

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export * from './rest/custom';
88
export * from './rest/info/general';
99
export * from './rest/info/perps';
1010
export * from './rest/info/spot';
11+
export * from './rest/info/leaderboard';
1112

1213
// Export WebSocket components
1314
export * from './websocket/connection';

src/rest/info.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export class InfoAPI {
1919
public readonly spot: SpotInfoAPI;
2020
public readonly perpetuals: PerpsInfoAPI;
2121
private readonly httpApi: HttpApi;
22-
private readonly generalAPI: GeneralInfoAPI;
22+
public readonly generalAPI: GeneralInfoAPI;
2323

2424
private readonly assetToIndexMap: Map<string, number>;
2525
private readonly exchangeToInternalNameMap: Map<string, string>;
@@ -74,6 +74,10 @@ export class InfoAPI {
7474
return this.generalAPI.getUserFills(user, raw_response);
7575
}
7676

77+
async getTradeInfo(user: string, orderId: number): Promise<any> {
78+
return this.generalAPI.getTradeInfo(user, orderId);
79+
}
80+
7781
async getUserFillsByTime(
7882
user: string,
7983
startTime: number,
@@ -83,6 +87,15 @@ export class InfoAPI {
8387
return this.generalAPI.getUserFillsByTime(user, startTime, endTime, raw_response);
8488
}
8589

90+
async getSpotUserFillsByTime(
91+
user: string,
92+
startTime: number,
93+
endTime: number,
94+
raw_response: boolean = false
95+
): Promise<UserFills> {
96+
return this.generalAPI.getUserFillsByTime(user, startTime, endTime, raw_response);
97+
}
98+
8699
async getUserRateLimit(user: string, raw_response: boolean = false): Promise<UserRateLimit> {
87100
return this.generalAPI.getUserRateLimit(user, raw_response);
88101
}

src/rest/info/general.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,15 @@ export class GeneralInfoAPI extends BaseInfoAPI {
7777
return raw_response ? response : this.convertSymbolsInObject(response);
7878
}
7979

80+
async getTradeInfo(user: string, orderId: number): Promise<any> {
81+
const response = await this.httpApi.makeRequest({
82+
type: INFO_TYPES.TRADE_INFO,
83+
user,
84+
orderId,
85+
});
86+
return this.convertSymbolsInObject(response);
87+
}
88+
8089
async getUserRateLimit(user: string, raw_response: boolean = false): Promise<UserRateLimit> {
8190
await this.ensureInitialized(raw_response);
8291
const response = await this.httpApi.makeRequest({ type: INFO_TYPES.USER_RATE_LIMIT, user }, 20);

0 commit comments

Comments
 (0)