Calculating Spot Volume
Calculate total trading volume for spot coins on Hyperliquid using the Allium API. Spot coins are identified as coins whose name starts with@.
Features
- Fetches all fills for a user using the
userFillsByTimeendpoint - Handles pagination (max 2000 results per call)
- Deduplicates fills using (time, coin, tid) as unique key
- Filters for spot coins (starting with
@) - Calculates trading volume as
price * size - Shows progress during execution
Usage
- Go
- Python
- JavaScript
Copy
Ask AI
go run main.go --api-key YOUR_API_KEY
# With custom user
go run main.go --api-key YOUR_API_KEY --user 0x...
# Build binary
go build -o spot_volume
./spot_volume --api-key YOUR_API_KEY
Copy
Ask AI
uv run spot_volume.py --api-key YOUR_API_KEY
# With custom user
uv run spot_volume.py --api-key YOUR_API_KEY --user 0x...
Copy
Ask AI
node spot_volume.js --api-key YOUR_API_KEY
# With custom user
node spot_volume.js --api-key YOUR_API_KEY --user 0x...
# Make executable
chmod +x spot_volume.js
./spot_volume.js --api-key YOUR_API_KEY
Arguments
| Argument | Required | Description |
|---|---|---|
--api-key | Yes | Allium API key |
--user | No | User wallet address (default: 0xc0142fc8aa609f324b44e414816ea549322afbe8) |
Output
The script displays:- Progress for each page fetched
- Number of fills received and processed
- Spot coin fills and volume per page
- Final summary with total statistics
Source Code
- Python
- Go
- JavaScript
Copy
Ask AI
#!/usr/bin/env python3
# /// script
# dependencies = [
# "requests",
# ]
# ///
"""
Fetch all fills for a Hyperliquid user and calculate total trading volume for spot coins.
Spot coins are identified by names starting with '@'.
"""
import argparse
import sys
from datetime import datetime
from typing import Set, Tuple
import requests
# API Configuration
API_URL = "https://api.allium.so/api/v1/developer/trading/hyperliquid/info/fills"
DEFAULT_USER = "0xc0142fc8aa609f324b44e414816ea549322afbe8"
# Start from a historical date (e.g., Hyperliquid launch)
START_TIME_MS = 1609459200000 # Jan 1, 2021
def fetch_fills(user: str, start_time: int, api_key: str, end_time: int | None = None) -> list:
"""Fetch fills from the API for a given time range."""
headers = {
"Content-Type": "application/json",
"X-API-KEY": api_key,
}
payload = {
"user": user,
"type": "userFillsByTime",
"startTime": start_time,
}
if end_time:
payload["endTime"] = end_time
print(" Making API request...", flush=True)
try:
response = requests.post(API_URL, json=payload, headers=headers, timeout=30)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Error fetching fills: {e}", file=sys.stderr)
if hasattr(e, 'response') and e.response is not None:
print(f"Response: {e.response.text}", file=sys.stderr)
sys.exit(1)
def calculate_spot_volume(user: str, api_key: str) -> None:
"""Calculate total trading volume for spot coins (coins starting with '@')."""
seen_fills: Set[Tuple[int, str, int]] = set() # (time, coin, tid)
total_volume = 0.0
spot_fill_count = 0
total_fill_count = 0
current_start_time = START_TIME_MS
page_count = 0
print(f"Fetching fills for user: {user}", flush=True)
print(f"Starting from: {datetime.fromtimestamp(START_TIME_MS / 1000).isoformat()}", flush=True)
print("-" * 80, flush=True)
while True:
page_count += 1
print(f"\nPage {page_count}: Fetching fills from {datetime.fromtimestamp(current_start_time / 1000).isoformat()}...", flush=True)
fills = fetch_fills(user, current_start_time, api_key)
if not fills:
print("No more fills found.", flush=True)
break
print(f" Received {len(fills)} fills", flush=True)
# Track new fills in this batch
new_fills = 0
new_spot_fills = 0
page_volume = 0.0
for fill in fills:
# Create unique key for this fill
fill_key = (fill.get("time"), fill.get("coin"), fill.get("tid"))
# Skip if we've already seen this fill
if fill_key in seen_fills:
continue
seen_fills.add(fill_key)
new_fills += 1
total_fill_count += 1
coin = fill.get("coin", "")
# Check if it's a spot coin (starts with '@')
if coin.startswith("@"):
sz = fill.get("sz", "0")
px = fill.get("px", "0")
try:
size = float(sz)
price = float(px)
volume = size * price # Trading volume = size * price
total_volume += volume
page_volume += volume
spot_fill_count += 1
new_spot_fills += 1
except (ValueError, TypeError):
print(f" Warning: Could not parse size '{sz}' or price '{px}' for coin {coin}", file=sys.stderr)
print(f" New unique fills: {new_fills} (spot: {new_spot_fills}, volume: {page_volume:.4f})", flush=True)
# Check if we need to continue paging
if len(fills) < 2000:
print("\nReceived less than 2000 fills, pagination complete.", flush=True)
break
if new_fills == 0:
print("\nNo new fills found, all remaining fills already seen. Pagination complete.", flush=True)
break
# Use the timestamp of the last fill for the next page
last_timestamp = fills[-1].get("time")
current_start_time = last_timestamp
print(f" Continuing from timestamp: {datetime.fromtimestamp(last_timestamp / 1000).isoformat()}", flush=True)
# Print results
print("\n" + "=" * 80)
print("RESULTS")
print("=" * 80)
print(f"Total fills processed: {total_fill_count:,}")
print(f"Spot coin fills: {spot_fill_count:,}")
print(f"Total spot coin trading volume: {total_volume:,.4f}")
print(f"Pages fetched: {page_count}")
print("=" * 80)
def main():
parser = argparse.ArgumentParser(
description="Calculate total trading volume for spot coins on Hyperliquid"
)
parser.add_argument(
"--user",
type=str,
default=DEFAULT_USER,
help=f"User wallet address (default: {DEFAULT_USER})"
)
parser.add_argument(
"--api-key",
type=str,
required=True,
help="Allium API key"
)
args = parser.parse_args()
# Validate user address format (basic check)
if not args.user.startswith("0x") or len(args.user) != 42:
print("Error: Invalid user address format. Expected 0x followed by 40 hex characters.", file=sys.stderr)
sys.exit(1)
calculate_spot_volume(args.user, args.api_key)
if __name__ == "__main__":
main()
Copy
Ask AI
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"regexp"
"strings"
"time"
)
const (
apiURL = "https://api.allium.so/api/v1/developer/trading/hyperliquid/info/fills"
defaultUser = "0xc0142fc8aa609f324b44e414816ea549322afbe8"
startTimeMS = int64(1609459200000) // Jan 1, 2021
maxResults = 2000
requestTimeout = 30 * time.Second
)
// Fill represents a single fill from the API
type Fill struct {
ClosedPnl *string `json:"closedPnl"`
Coin *string `json:"coin"`
Crossed *bool `json:"crossed"`
Dir *string `json:"dir"`
Hash *string `json:"hash"`
Oid *int64 `json:"oid"`
Px *string `json:"px"`
Side *string `json:"side"`
StartPosition *string `json:"startPosition"`
Sz *string `json:"sz"`
Time *int64 `json:"time"`
Fee *string `json:"fee"`
FeeToken *string `json:"feeToken"`
Tid *int64 `json:"tid"`
BuilderFee *string `json:"builderFee"`
BuilderAddress *string `json:"builderAddress"`
TwapId *int64 `json:"twapId"`
}
// FillRequest represents the API request payload
type FillRequest struct {
Type string `json:"type"`
User string `json:"user"`
StartTime int64 `json:"startTime"`
EndTime *int64 `json:"endTime,omitempty"`
}
// FillKey uniquely identifies a fill
type FillKey struct {
Time int64
Coin string
Tid int64
}
func main() {
user := flag.String("user", defaultUser, "User wallet address")
apiKey := flag.String("api-key", "", "Allium API key")
flag.Parse()
if *apiKey == "" {
fmt.Fprintln(os.Stderr, "Error: --api-key is required")
flag.Usage()
os.Exit(1)
}
// Validate user address format
if !isValidAddress(*user) {
fmt.Fprintln(os.Stderr, "Error: Invalid user address format. Expected 0x followed by 40 hex characters.")
os.Exit(1)
}
calculateSpotVolume(*user, *apiKey)
}
func isValidAddress(address string) bool {
matched, _ := regexp.MatchString(`^0x[0-9a-fA-F]{40}$`, address)
return matched
}
func calculateSpotVolume(user, apiKey string) {
seenFills := make(map[FillKey]bool)
var totalVolume float64
var spotFillCount int
var totalFillCount int
currentStartTime := startTimeMS
pageCount := 0
fmt.Printf("Fetching fills for user: %s\n", user)
fmt.Printf("Starting from: %s\n", time.UnixMilli(startTimeMS).Format(time.RFC3339))
fmt.Println(strings.Repeat("-", 80))
for {
pageCount++
fmt.Printf("\nPage %d: Fetching fills from %s...\n", pageCount, time.UnixMilli(currentStartTime).Format(time.RFC3339))
fills, err := fetchFills(user, currentStartTime, apiKey)
if err != nil {
fmt.Fprintf(os.Stderr, "Error fetching fills: %v\n", err)
os.Exit(1)
}
if len(fills) == 0 {
fmt.Println("No more fills found.")
break
}
fmt.Printf(" Received %d fills\n", len(fills))
// Track new fills in this batch
newFills := 0
newSpotFills := 0
var pageVolume float64
for _, fill := range fills {
// Create unique key for this fill
if fill.Time == nil || fill.Coin == nil || fill.Tid == nil {
continue
}
fillKey := FillKey{
Time: *fill.Time,
Coin: *fill.Coin,
Tid: *fill.Tid,
}
// Skip if we've already seen this fill
if seenFills[fillKey] {
continue
}
seenFills[fillKey] = true
newFills++
totalFillCount++
coin := *fill.Coin
// Check if it's a spot coin (starts with '@')
if strings.HasPrefix(coin, "@") {
if fill.Sz == nil || fill.Px == nil {
fmt.Fprintf(os.Stderr, " Warning: Missing size or price for coin %s\n", coin)
continue
}
var size, price float64
if _, err := fmt.Sscanf(*fill.Sz, "%f", &size); err != nil {
fmt.Fprintf(os.Stderr, " Warning: Could not parse size '%s' for coin %s\n", *fill.Sz, coin)
continue
}
if _, err := fmt.Sscanf(*fill.Px, "%f", &price); err != nil {
fmt.Fprintf(os.Stderr, " Warning: Could not parse price '%s' for coin %s\n", *fill.Px, coin)
continue
}
volume := size * price // Trading volume = size * price
totalVolume += volume
pageVolume += volume
spotFillCount++
newSpotFills++
}
}
fmt.Printf(" New unique fills: %d (spot: %d, volume: %.4f)\n", newFills, newSpotFills, pageVolume)
// Check if we need to continue paging
if len(fills) < maxResults {
fmt.Println("\nReceived less than 2000 fills, pagination complete.")
break
}
if newFills == 0 {
fmt.Println("\nNo new fills found, all remaining fills already seen. Pagination complete.")
break
}
// Use the timestamp of the last fill for the next page
lastFill := fills[len(fills)-1]
if lastFill.Time == nil {
fmt.Fprintln(os.Stderr, "Error: Last fill has no timestamp")
os.Exit(1)
}
currentStartTime = *lastFill.Time
fmt.Printf(" Continuing from timestamp: %s\n", time.UnixMilli(currentStartTime).Format(time.RFC3339))
}
// Print results
fmt.Println("\n" + strings.Repeat("=", 80))
fmt.Println("RESULTS")
fmt.Println(strings.Repeat("=", 80))
fmt.Printf("Total fills processed: %s\n", formatNumber(totalFillCount))
fmt.Printf("Spot coin fills: %s\n", formatNumber(spotFillCount))
fmt.Printf("Total spot coin trading volume: %s\n", formatFloat(totalVolume))
fmt.Printf("Pages fetched: %d\n", pageCount)
fmt.Println(strings.Repeat("=", 80))
}
func fetchFills(user string, startTime int64, apiKey string) ([]Fill, error) {
fmt.Println(" Making API request...")
request := FillRequest{
Type: "userFillsByTime",
User: user,
StartTime: startTime,
}
jsonData, err := json.Marshal(request)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-KEY", apiKey)
client := &http.Client{
Timeout: requestTimeout,
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
var fills []Fill
if err := json.Unmarshal(body, &fills); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return fills, nil
}
func formatNumber(n int) string {
str := fmt.Sprintf("%d", n)
var result []rune
for i, r := range str {
if i > 0 && (len(str)-i)%3 == 0 {
result = append(result, ',')
}
result = append(result, r)
}
return string(result)
}
func formatFloat(f float64) string {
str := fmt.Sprintf("%.4f", f)
parts := strings.Split(str, ".")
intPart := parts[0]
decPart := parts[1]
var result []rune
for i, r := range intPart {
if i > 0 && (len(intPart)-i)%3 == 0 {
result = append(result, ',')
}
result = append(result, r)
}
return string(result) + "." + decPart
}
Copy
Ask AI
#!/usr/bin/env node
/**
* Fetch all fills for a Hyperliquid user and calculate total trading volume for spot coins.
* Spot coins are identified by names starting with '@'.
*/
const API_URL = 'https://api.allium.so/api/v1/developer/trading/hyperliquid/info/fills';
const DEFAULT_USER = '0xc0142fc8aa609f324b44e414816ea549322afbe8';
const START_TIME_MS = 1609459200000; // Jan 1, 2021
const MAX_RESULTS = 2000;
const REQUEST_TIMEOUT = 30000;
/**
* Fetch fills from the API for a given time range
*/
async function fetchFills(user, startTime, apiKey, endTime = null) {
const payload = {
user,
type: 'userFillsByTime',
startTime,
};
if (endTime) {
payload.endTime = endTime;
}
console.log(' Making API request...');
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-KEY': apiKey,
},
body: JSON.stringify(payload),
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
const text = await response.text();
throw new Error(`API returned status ${response.status}: ${text}`);
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
}
/**
* Calculate total trading volume for spot coins
*/
async function calculateSpotVolume(user, apiKey) {
const seenFills = new Set(); // Set of "time:coin:tid" strings
let totalVolume = 0.0;
let spotFillCount = 0;
let totalFillCount = 0;
let currentStartTime = START_TIME_MS;
let pageCount = 0;
console.log(`Fetching fills for user: ${user}`);
console.log(`Starting from: ${new Date(START_TIME_MS).toISOString()}`);
console.log('-'.repeat(80));
while (true) {
pageCount++;
console.log(`\nPage ${pageCount}: Fetching fills from ${new Date(currentStartTime).toISOString()}...`);
let fills;
try {
fills = await fetchFills(user, currentStartTime, apiKey);
} catch (error) {
console.error(`Error fetching fills: ${error.message}`);
process.exit(1);
}
if (!fills || fills.length === 0) {
console.log('No more fills found.');
break;
}
console.log(` Received ${fills.length} fills`);
// Track new fills in this batch
let newFills = 0;
let newSpotFills = 0;
let pageVolume = 0.0;
for (const fill of fills) {
// Create unique key for this fill
const fillKey = `${fill.time}:${fill.coin}:${fill.tid}`;
// Skip if we've already seen this fill
if (seenFills.has(fillKey)) {
continue;
}
seenFills.add(fillKey);
newFills++;
totalFillCount++;
const coin = fill.coin || '';
// Check if it's a spot coin (starts with '@')
if (coin.startsWith('@')) {
const sz = fill.sz || '0';
const px = fill.px || '0';
try {
const size = parseFloat(sz);
const price = parseFloat(px);
if (isNaN(size) || isNaN(price)) {
console.error(` Warning: Could not parse size '${sz}' or price '${px}' for coin ${coin}`);
continue;
}
const volume = size * price; // Trading volume = size * price
totalVolume += volume;
pageVolume += volume;
spotFillCount++;
newSpotFills++;
} catch (error) {
console.error(` Warning: Error processing coin ${coin}: ${error.message}`);
}
}
}
console.log(` New unique fills: ${newFills} (spot: ${newSpotFills}, volume: ${pageVolume.toFixed(4)})`);
// Check if we need to continue paging
if (fills.length < MAX_RESULTS) {
console.log('\nReceived less than 2000 fills, pagination complete.');
break;
}
if (newFills === 0) {
console.log('\nNo new fills found, all remaining fills already seen. Pagination complete.');
break;
}
// Use the timestamp of the last fill for the next page
const lastFill = fills[fills.length - 1];
currentStartTime = lastFill.time;
console.log(` Continuing from timestamp: ${new Date(currentStartTime).toISOString()}`);
}
// Print results
console.log('\n' + '='.repeat(80));
console.log('RESULTS');
console.log('='.repeat(80));
console.log(`Total fills processed: ${formatNumber(totalFillCount)}`);
console.log(`Spot coin fills: ${formatNumber(spotFillCount)}`);
console.log(`Total spot coin trading volume: ${formatFloat(totalVolume)}`);
console.log(`Pages fetched: ${pageCount}`);
console.log('='.repeat(80));
}
/**
* Format number with comma separators
*/
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
/**
* Format float with comma separators and 4 decimal places
*/
function formatFloat(num) {
const [intPart, decPart] = num.toFixed(4).split('.');
return formatNumber(intPart) + '.' + decPart;
}
/**
* Validate Ethereum address format
*/
function isValidAddress(address) {
return /^0x[0-9a-fA-F]{40}$/.test(address);
}
/**
* Parse command line arguments
*/
function parseArgs() {
const args = process.argv.slice(2);
const parsed = {
user: DEFAULT_USER,
apiKey: null,
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--user' && i + 1 < args.length) {
parsed.user = args[++i];
} else if (arg === '--api-key' && i + 1 < args.length) {
parsed.apiKey = args[++i];
} else if (arg === '--help' || arg === '-h') {
console.log(`
Usage: node spot_volume.js [options]
Calculate total trading volume for spot coins on Hyperliquid
Options:
--user <address> User wallet address (default: ${DEFAULT_USER})
--api-key <key> Allium API key (required)
--help, -h Show this help message
`);
process.exit(0);
}
}
return parsed;
}
/**
* Main entry point
*/
async function main() {
const args = parseArgs();
if (!args.apiKey) {
console.error('Error: --api-key is required');
console.error('Use --help for usage information');
process.exit(1);
}
if (!isValidAddress(args.user)) {
console.error('Error: Invalid user address format. Expected 0x followed by 40 hex characters.');
process.exit(1);
}
await calculateSpotVolume(args.user, args.apiKey);
}
// Run main function
main().catch((error) => {
console.error(`Fatal error: ${error.message}`);
process.exit(1);
});