メインコンテンツへスキップ

リアルタイムのハイパーリキッド・ポートフォリオ・トラッカーを構築する

更新日:
2026年4月22日

読了時間:17分

概要

Hyperliquidで常時取引を行うトレーダーにとって、ポジション、PnL、証拠金使用率をリアルタイムで監視するためには、包括的なポートフォリオトラッカーが不可欠です。このガイドでは、QuicknodeのHyperliquid情報エンドポイントを使用して、任意のHyperliquidウォレットを監視する強力なポートフォリオトラッカーの構築方法をご紹介します。 このチュートリアルでは、便利な取引ツールの作成にとどまらず、アプリケーション構築のためにHyperCoreデータがどのように取得・構造化・活用されるのかについても解説します。このアプリケーションでは、以下の機能が紹介されています:


  • ポジションのリアルタイム追跡- パーペチュアルポジションの損益(PnL)に関するリアルタイムの更新情報
  • ポートフォリオ分析- 口座残高、証拠金使用状況、およびリスク指標
  • 金庫管理- 金庫の資産価値と保管スケジュールを追跡する
  • Spot Holdings- トークンの残高と米ドル換算額を監視する
  • 任意のウォレットを検索- 異なる取引口座を切り替える

トップページ


ダッシュボードページ

主な業務内容

4つの段階を経て、完全なポートフォリオ管理ツールを構築しましょう:


  1. QuicknodeエンドポイントとSupabaseアカウントの設定
  2. データベーススキーマの設定
  3. 500ミリ秒ごとにHyperCoreデータを取得するインデクサーの作成
  4. 最新のUIライブラリを使用して、リアルタイムの取引データを表示するダッシュボードを構築する

必要なもの


Quicknodeのエンドポイントを選ぶ理由とは?

Quicknode は、独自のノードを実行する必要をなくす、Hyperliquid 専用の API エンドポイントを提供しています:

  • 設定不要の事前設定済みエンドポイント
  • 接続管理とフェイルオーバーを処理する
  • 追加のインフラを必要とせずに、HyperCoreのデータに直接アクセス可能

アーキテクチャの概要

ポートフォリオ・トラッカーは、PostgreSQLデータベースを介して通信を行う3つのコンポーネントで構成されています。インデクサーはHyperliquidからデータを取得してデータベースに格納し、フロントエンドは表示のためにデータベースにクエリを実行します。

技術スタックの構成要素

  • フロントエンド:React + TypeScript + Tailwind CSS + shadcn/ui および Radix UI

    • レスポンシブなインターフェースで取引データを表示します
    • 1000ミリ秒ごとに投票データベースを照会して更新を確認する
    • データベースへのリクエストを通じて、ウォレットの切り替えを処理する
  • バックエンド:ポーリング間隔500msのNode.jsインデクサー

    • 5つの異なるHyperliquidエンドポイントからデータを取得します
    • 適切な精度処理を行って、データをPostgreSQLに格納します
    • フロントエンドからのウォレット切り替えリクエストを処理する
世論調査に関する留意点

このガイドでは、リアルタイムの更新を実演するために、短いポーリング間隔(インデクサーは500ms、フロントエンドは1000ms)を使用しています。必要に応じて、これらの間隔を調整することができます:

  • フロントエンド: src/Dashboard.tsx 260行目~264行目 - 以下の部分を変更する 1000 値を入力してください:
    const interval = setInterval(async () => {
    await fetchData(currentWallet);
    }, 1000);
  • インデクサー: src/indexer/indexer.ts 623行目~630行目 - 以下の部分を変更する 500 値を入力してください:
    setInterval(async () => {
    await indexer.checkForWalletSwitch();
    await indexer.indexData();
    }, 500);

QuicknodeとSupabaseの利用状況を監視し、コストを最適化しましょう。

  • データベース:Supabase PostgreSQL

    • データを6つのテーブルに、財務上の精度で格納しています(DECIMAL (種類)
    • フロントエンドとインデクサー間の通信を、以下を介して処理します wallet_switch_requests
    • 一意性制約を使用して、重複するエントリを防ぐ
  • データソース: ハイパーリキッド 情報 Quicknode経由のエンドポイント

    • 口座データ、ポジション、保管資産、スポット残高、および委任情報を提供します
    • 精度を表す数値を文字列として含むJSON形式のデータを返す
    • ウォレットアドレスをパラメータとして指定したHTTP POSTリクエストを介してアクセスします
                    ┌─────────────────┐
│ 犯人トレーダー │
└─────────┬───────┘
│ 1. ウォレットアドレスを入力

┌─────────────────┐
│ Reactダッシュボード │◄─────────────────┐
└─────────┬───────┘ │
│ 2. リクエストの保存 │ 6. データの読み取りと表示
▼ │
┌─────────────────┐ │
│ Supabase │◄─────────────────┤
│ PostgreSQL │ │
└─────────┬───────┘ │
│ 3. リクエストの検出 │ 5. データの保存
▼ │
┌─────────────────┐ │
│ インデクサー │──────────────────┘
│ (500ms ポーリング) │
└─────────┬───────┘
│ 4. HyperCoreデータの取得

┌─────────────────┐
│ Quicknode │
│ Hyperliquid │
│ エンドポイント │
└─────────────────┘

申請の流れ

1回目の財布の捜索:


  1. ユーザーがウォレットを入力→ フロントエンドが検証を行い、リクエストをデータベースに保存する
  2. インデクサーがリクエストを検知(500ミリ秒ごとにポーリング)→ 新しいウォレットに切り替わる
  3. データ収集を開始→ 指定された5つのエンドポイントから個別にデータを取得し、データベースに保存する
  4. フロントエンドがデータベースをポーリング(1000ミリ秒ごと)→ インデクサーから取得した最新のデータでUIが更新される

第2回 ウォレット検索とその先:


  1. ユーザーが新しいウォレットを入力→ フロントエンドが以前のウォレットのデータをすべて消去し、インデクサーに新しいウォレットへの切り替えを指示する
  2. インデクサーが新しいリクエストを検知→ 直ちに新しいウォレットに切り替え、新しいウォレットアドレスを使用してデータの取得を開始する
  3. データがリアルタイムで更新される→ ダッシュボードが新しいウォレットのポジションや指標の取得を開始する

プロジェクトの構成

このプロジェクトは、データ収集、UIコンポーネント、ビジネスロジックを分離した、すっきりとしたモジュール型のアーキテクチャを採用しています。この構造により、コードベースの保守性が向上し、機能の拡張も容易になります。

ディレクトリの内訳

├── src/
│ ├── indexer/
│ │ ├── indexer.ts # メインのインデクサーのオーケストレーションおよびウォレット管理
│ │ └── apicalls.ts # Hyperliquid情報エンドポイントへのクエリ
│ ├── components/
│ │ ├── ui/ # shadcn/ui コンポーネント(Button、Input、Card など)
│ │ └── dashboard/ # ダッシュボードコンポーネント(WalletHeader、PortfolioMetricsなど)
│ ├── shared/
│ │ ├── types.ts # TypeScriptのインターフェースと型
│ │ ├── utils.ts # 書式設定、計算、およびユーティリティ関数
│ │ ├── constants.ts # ダッシュボード用の UI 定数
│ │ └── supabase.ts # フロントエンドからのアクセス用 Supabase クライアントインスタンス
│ ├── Dashboard.tsx # メインダッシュボードのロジック
│ └── main.tsx
├── supabase/
│ └── schema.sql # 完全なデータベーススキーマ
├── package.json
└── .env

プロジェクトの実施

ステップ 1: リポジトリをクローンする

まず、プロジェクトのリポジトリをクローンし、プロジェクトディレクトリに移動します:

git clone https://github.com/quiknode-labs/qn-guide-examples.git
cd qn-guide-examples/sample-dapps/hyperliquid-portfolio-tracker

ステップ 2:環境設定ファイルの設定

あなただけの .env 次のコマンドを実行してファイルを作成します:

cp .env.example .env

ステップ 3: Supabase データベースの設定

Supabaseのウェブサイトで、新しいSupabaseアカウントを作成するか、既存のSupabaseアカウントにログインしてください。

新しいプロジェクトを作成し、[接続] ボタンをクリックします。

DB Connect


「App Frameworks」セクションで「React」を選択し、 using フィールドをViteにコピーします。以下の内容をコピーして VITE_SUPABASE_URL そして VITE_SUPABASE_ANON_KEY 値を計算し、それらを .env ファイル。

DB selectReact

最後に、右上隅にあるSQLエディタに移動し、 schema.sql ファイルを開き、「実行」をクリックします。これにより、フロントエンド向けのデータの保存および取得に必要なすべてのテーブルと関数が作成されます。

SQLエディタ

ステップ4:Quicknodeの設定

無料トライアルを申し込む Quicknodeアカウント, その後、最初のHyperliquid RPCエンドポイントを作成し、それを .env ファイル。

情報

必ず既存のものを削除してください /evm そして、以下を追加します /info Hyperliquidの情報エンドポイントにアクセスするには、QuicknodeのエンドポイントURLの末尾に以下を追加してください。

ステップ 5:アプリケーションを起動する

テーブルの作成と環境の設定が完了したら、ルートディレクトリで以下のコマンドを実行してプロジェクトを開始できます:

npm install && npm run dev:both

これにより、フロントエンドアプリケーションとインデクサーの両方が実行されます。

インデクサーの実行が開始されると、有効なウォレットアドレスが検索されるのを待機します:

インデクサーの起動中

localhost の URL でフロントエンドページを開き、「デモウォレット」ボタンをクリックして、ウォレットアドレスの例を確認してください:

財布の中を探す

ウォレットを検索した後、インデクサーはそのアドレスのデータを500ミリ秒ごとに取得し始めます:

インデクサーが実行中

ダッシュボードには、ウォレットのリアルタイム統計情報が表示されます:

ダッシュボードページ

これで、口座残高、保有ポジション、その他の取引データをリアルタイムで確認できるようになりました。

トラブルシューティング

インデクサーを使用する際、セットアップ時または実行時に次のような問題が発生する場合があります:


  • インデクサーが応答しなくなる
  • ウォレットを検索してもデータが表示されない
  • 誤ってインデクサーを停止させてしまった

解決策:

次のコマンドを実行して、インデクサーを再起動してください:

npm run dev:indexer

その場合は、有効なウォレットアドレスを入力して、もう一度検索してみてください。

コードベースの詳細

ポートフォリオ・トラッカーの稼働が始まったところで、その内部の仕組みについて見ていきましょう。このセクションでは、リアルタイムのポートフォリオ追跡を可能にする3つの主要な構成要素について詳しく解説します:

インデクサー - Hyperliquidの 情報 エンドポイントを通じて apicalls.ts、ウォレット切り替えのリクエストを検出し、500ミリ秒ごとに5つの異なるHyperliquidエンドポイントからのデータ収集を調整します。

スキーマとデータベース- 金融レベルの精度で取引データを格納するPostgreSQLのテーブル構造、およびフロントエンドとインデクサー間の連携メカニズムについて解説します。

ダッシュボード(フロントエンド)- Reactによる状態管理、リアルタイムデータベースのポーリング、ウォレットアドレスの検証、およびリアルタイムの取引データを表示するユーザーインターフェースについて解説します。

インデクサー

インデクサーは独立したNode.jsプロセスとして実行され、500ミリ秒ごとにQuicknodeのエンドポイントをポーリングします。インデクサーは、現在のウォレットアドレスの取引データを取得し、データベースに保存します。また、フロントエンドからのウォレット切り替えリクエストを待機し、ロックファイルを使用してプロセスの分離を処理します。

財布内の小銭の増減検知

インデクサーは wallet_switch_requests 500ミリ秒ごとにテーブルを監視してフロントエンドのウォレット切り替えを検出し、切り替え処理中の競合状態を防ぐためにステータスフィールドを使用する。

ハイパーリキッド 情報 エンドポイント統合

インデクサーは HyperliquidAPI class in apicalls.ts QuicknodeのHyperliquidエンドポイントと通信するためです。各APIメソッドは、さまざまな種類の取引データを取得するために、一貫したパターンに従っています:

// From apicalls.ts - Main account data fetching
async getClearinghouseState(walletAddress: string): Promise<ClearinghouseStateResponse> {
const payload = {
type: 'clearinghouseState',
user: walletAddress
};

const response = await fetch(QUICKNODE_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});

return await response.json();
}

その getUserVaultEquities() このメソッドは同じ構造を採用していますが、 type: 'userVaultEquities', 各エンドポイントにはウォレットアドレスとエンドポイントの種類のみが必要であることを示しています。すべてのメソッドには、取引データに関する問題をデバッグするための包括的なエラー処理とロギング機能が備わっています。

インデクサーについて詳しく解説

メインのインデックス作成ループは、5つの異なる 情報 エンドポイントからデータを取得し、その結果を個別のデータベーステーブルに格納します:

// From indexer.ts - Core data fetching and storage
const data = await hyperliquidAPI.getClearinghouseState(CURRENT_WALLET_ADDRESS);
const stateId = await this.storeClearinghouseState(data);

// Store positions with atomic replacement to prevent UI flickering
await this.storeAssetPositions(data.assetPositions, data.time);

// Fetch additional data types with error handling
try {
const rateLimitData = await hyperliquidAPI.getUserRateLimit(CURRENT_WALLET_ADDRESS);
await this.storeUserRateLimit(rateLimitData, timestamp);
} catch (error) {
console.log(`No rate limit data available for ${CURRENT_WALLET_ADDRESS}`);
}

インデクサーは個々のエンドポイントの障害に対して柔軟に対応します。つまり、あるデータソースで障害が発生しても、他のデータソースは通常通り動作し続けます。

インデクサーは、複数のインスタンスが同時に実行されるのを防ぐために、ファイルベースのロックを使用しています:

// From indexer.ts - Lock file creation and process checking
function createLock(): boolean {
if (fs.existsSync(LOCK_FILE)) {
const { pid } = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf8'));
try {
process.kill(pid, 0);
console.error(`❌ Another indexer is already running (PID: ${pid})`);
return false;
} catch (e) {
fs.unlinkSync(LOCK_FILE); // Remove stale lock
}
}

fs.writeFileSync(LOCK_FILE, JSON.stringify({ pid: process.pid }));
return true;
}

インデクサーは、ロックファイルを使用して複数のインスタンスが同時に実行されるのを防ぎ、データの整合性を確保します。

スキーマとデータベース(Supabase)

インデクサーがデータを収集する仕組みについて確認したところで、そのデータがどのように保存され、構成されているのかを見ていきましょう。PostgreSQLデータベースは、取引データを6つのテーブルに保存し、フロントエンドとインデクサー間の通信を処理します。各テーブルでは DECIMAL 財務上の精度を確保するためのデータ型と、重複入力を防ぐための独自の制約。

テーブル構造の例

資産ポジション表- 財務上の精度をもって、永久取引ポジションを記録します:

-- From schema.sql - Core trading data storage
CREATE TABLE asset_positions (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
wallet_address TEXT NOT NULL,

coin TEXT NOT NULL, -- Asset symbol (e.g., 'BTC', 'ETH', 'SOL')
size DECIMAL(20, 5) NOT NULL, -- Position size: 20 digits total, 5 after decimal
leverage_type TEXT NOT NULL, -- 'cross' or 'isolated' margin mode
leverage_value INTEGER NOT NULL, -- Leverage multiplier (1x, 5x, 10x, etc.)

entry_price DECIMAL(20, 5), -- Average entry price with 5 decimal precision
position_value DECIMAL(20, 5), -- Current USD value of the position
unrealized_pnl DECIMAL(20, 5), -- Profit/loss before closing position
liquidation_price DECIMAL(20, 5), -- Price at which position gets liquidated
margin_used DECIMAL(20, 5), -- Amount of margin allocated to this position

timestamp BIGINT NOT NULL, -- Unix timestamp in milliseconds from HyperCore
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() -- Database insertion time
);

-- Unique constraint prevents duplicate positions per wallet-coin pair
ALTER TABLE asset_positions ADD CONSTRAINT unique_position_per_wallet
UNIQUE (wallet_address, coin);

その DECIMAL(20, 5) このタイプは、小数点以下5桁を含む合計20桁の数字を扱います。

ウォレット切り替えリクエストテーブル- フロントエンドとインデクサー間の通信を調整します:

-- schema.sql より - フロントエンドとインデクサーの連携
CREATE TABLE wallet_switch_requests (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
requested_wallet_address TEXT NOT NULL, -- イーサリアムアドレス(0x形式)
status TEXT NOT NULL DEFAULT '保留中', -- ステートマシン: 保留中 → 処理中 → 完了
作成日時 TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- インデクサーによる効率的なステータスベースのクエリのためのインデックス
CREATE INDEX idx_wallet_switch_requests_status ON wallet_switch_requests(status);

このテーブルは、ReactフロントエンドとNode.jsインデクサー間のメッセージキューとして機能します。ユーザーが新しいウォレットアドレスを入力すると、フロントエンドは 「保留中」 リクエスト。インデクサーは 「保留中」 500ミリ秒ごとにリクエストを行い、ステータスを 「処理」 競合状態を防ぐため、その後ウォレットの切り替えを行います。このステータスの進行プロセスにより、ユーザーが次々と異なるアドレスを検索した場合でも、一度に1つのウォレット切り替えのみが行われることが保証されます。

ダッシュボード(フロントエンド)

インデクサーがデータを収集し、データベースがそれを保存する中で、最後のピースとなるのがユーザーインターフェースです。Reactによるフロントエンドは、1000ミリ秒ごとにデータベースをポーリングして最新の取引データを取得し、モジュール化されたコンポーネントを使用してそれを表示します。また、データベースにリクエストを挿入し、ローカル状態を直ちにクリアすることで、ウォレットの切り替えを管理しています。

概要

ダッシュボードのアーキテクチャは、モジュール式のコンポーネント構成を備えた一元的な状態管理を中核としています:

// From Dashboard.tsx - Centralized state management
const [latestState, setLatestState] = useState<ClearinghouseState | null>(null);
const [positions, setPositions] = useState<AssetPosition[]>([]);
const [vaultEquities, setVaultEquities] = useState<UserVaultEquity[]>([]);
const [spotBalances, setSpotBalances] = useState<SpotBalance[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [hasInitialData, setHasInitialData] = useState(false);

その ダッシュボード このコンポーネントは、Reactのステートにすべての取引データを保持し、それをプロパティとして子コンポーネントに渡します。このパターンにより、ステートの管理が簡素化され、デバッグ時のデータフローの追跡も容易になります。

自動更新ポーリング

フロントエンドは、 setInterval 適切なクリーンアップ処理を含むループ:

// From Dashboard.tsx - Auto-refresh with cleanup
useEffect(() => {
if (currentWallet && hasStarted && hasInitialData) {
const interval = setInterval(async () => {
await fetchData(currentWallet);
}, 1000);
return () => clearInterval(interval);
}
}, [currentWallet, hasStarted, hasInitialData, fetchData]);

// Data freshness detection provides visual feedback
const isDataStale = latestState && (Date.now() - latestState.timestamp > 3000);

ポーリングはユーザーの操作に応じて自動的に開始・停止され、リソースを節約するため、必要な時のみ実行されます。インターフェースには、データが古くなっている場合(3秒以上経過している場合)が表示され、ユーザーにデータの鮮度を常に把握してもらえます。

データベースクエリのパターン

フロントエンドでは、最適化されたクエリパターンを使用して、Supabaseから取引データをリアルタイムで取得しています:

// From Dashboard.tsx - Real-time data queries
const { data: latestData, error: latestError } = await supabase
.from('clearinghouse_states')
.select('*')
.eq('wallet_address', walletAddress)
.order('timestamp', { ascending: false })
.limit(1)
.maybeSingle();

// Get all positions for this wallet
const { data: positionsData, error: positionsError } = await supabase
.from('asset_positions')
.select('*')
.eq('wallet_address', walletAddress)
.order('timestamp', { ascending: false });

if (positionsError && positionsError.code !== 'PGRST116') throw positionsError;

クエリは、ウォレットに取引データがない場合にも適切に処理され、エラーを表示する代わりに空の結果を返します。

ウォレットの切り替え管理

フロントエンドはウォレットアドレスを検証し、切り替え時に直ちに状態をクリアします:

// From Dashboard.tsx - Wallet validation and switching
const isValidWalletAddress = (address: string): boolean => {
return /^0x[a-fA-F0-9]{40}$/.test(address);
};

const handleWalletSearch = async () => {
if (!isValidWalletAddress(address)) {
setError('Invalid wallet address format');
return;
}

// Clear ALL old data immediately when switching wallets
setLatestState(null);
setPositions([]);
setVaultEquities([]);
setSpotBalances([]);
setIsSearching(true);

// Signal indexer to switch
await switchIndexerWallet(address);
setCurrentWallet(address);
};

このインターフェースはウォレットアドレスを検証し、切り替え時に古いデータを即座にクリアし、新しいデータが読み込まれるまで読み込み中の状態を表示します。

データの表示

Reactのステートから、情報を整形して表示する専用のUIコンポーネントへとデータが流れ込みます:

// From PortfolioMetrics.tsx - Portfolio overview card component
interface PortfolioMetricsProps {
totalAccountValue: number;
totalUnrealizedPnl: number;
userRateLimit: UserRateLimit | null;
vaultEquities: UserVaultEquity[];
delegations: Delegation[];
formatCurrency: (value: number) => string;
}

export const PortfolioMetrics: React.FC<PortfolioMetricsProps> = ({
totalAccountValue,
totalUnrealizedPnl,
formatCurrency
}) => {
return (
<Card className="bg-slate-900/50 border-slate-700/50 backdrop-blur-sm mb-6">
<CardContent className="p-4">
<div className="text-xs text-slate-400 mb-3 font-medium tracking-wide uppercase">
Perp Account Value
</div>
<div className="text-2xl font-bold text-white">
{formatCurrency(totalAccountValue)}
</div>
</CardContent>
</Card>
);
};

その ダッシュボード コンポーネントは、計算された値や書式設定関数を子コンポーネントに渡します。子コンポーネントは、取引データの視覚的な表示やスタイル設定を処理します。

結論

おめでとうございます!QuicknodeのHyperliquid情報エンドポイントを使用して、リアルタイムのHyperliquidポートフォリオトラッカーの構築に成功しました。この過程で、永久先物取引データの取得方法、金融分野の精度に適合したPostgreSQLデータベースの構築方法、そしてリアルタイムで更新されるレスポンシブなダッシュボードの構築方法を学びました。この基礎を活かすことで、自動リスク監視や複数ウォレットの比較といった高度な取引ツールの実現が可能になります。

Rechartsなどのチャート作成ライブラリを統合したり、お気に入りの通知サービスを使って取引アラートを構築したりすることで、このプロジェクトをさらに拡張することができます。Hyperliquid を活用したその他の方法については、当社の他のHyperliquid ガイドをご覧ください。

今後の手順

ポートフォリオ管理ツールが動作するようになったところで、ここではこのアプリケーションを拡張・改善するためのいくつかの方法をご紹介します:

  • ポジションが危険な証拠金水準に近づいた際の清算警告
  • グラフを用いたポートフォリオのパフォーマンス推移の追跡
  • ウォレット検索間の読み込み時間が短縮されました

その他の参考資料


行き詰まったり質問がある場合は、Discordに投稿してください。Twitter(@Quicknode)やTelegramの告知チャンネルをフォローして、最新情報をチェックしてください。

皆様からのフィードバックを心よりお待ちしております!❤️

ご意見や新しいトピックに関するご要望などがありましたら、ぜひお知らせください。皆様からのご連絡を心よりお待ちしております。

このガイドをシェアする