构建一个实时的 Hyperliquid 投资组合跟踪器

本文介绍了如何使用QuickNode的Hyperliquid信息端点构建一个Hyperliquid投资组合跟踪器,该跟踪器可以实时监控任何Hyperliquid钱包的持仓、盈亏和保证金利用率。文章详细阐述了如何获取、构建和利用HyperCore数据,并展示了实时持仓跟踪、投资组合分析、保险库管理和现货持有等功能的实现。

概述

作为 Hyperliquid 上的永续合约交易者,拥有一个全面的投资组合跟踪器对于实时监控你的仓位、盈亏和保证金利用率至关重要。本指南将向你展示如何使用 QuickNode 的 Hyperliquid 信息端点构建一个强大的投资组合跟踪器,以监控任何 Hyperliquid 钱包。除了创建一个有用的交易工具外,本教程还将揭秘如何获取、构建和利用 HyperCore 数据来构建应用程序。该应用程序展示了:

  • 实时仓位跟踪 - 实时更新永续合约仓位以及盈亏
  • 投资组合分析 - 账户价值、保证金使用情况和风险指标
  • 金库管理 - 跟踪金库价值和锁仓计划
  • 现货持有量 - 监控代币余额和美元价值
  • 搜索任何钱包 - 在不同的交易账户之间切换

主页

仪表盘页

你将要做什么

分 4 个阶段构建一个完整的投资组合跟踪器:

  1. 设置 QuickNode 端点和 Supabase 账户
  2. 设置你的数据库模式
  3. 创建一个每 500 毫秒获取 HyperCore 数据的索引器
  4. 使用现代 UI 库构建一个仪表板,显示实时交易数据

你将需要什么

为什么选择 QuickNode 端点?

QuickNode 提供专用的 Hyperliquid API 端点,无需运行你自己的节点:

  • 预配置的端点,无需任何设置
  • 处理连接管理和故障转移
  • 无需额外基础设施即可直接访问 HyperCore 数据

架构概述

投资组合跟踪器由三个组件组成,它们通过 PostgreSQL 数据库进行通信。索引器从 Hyperliquid 获取数据,将其存储在数据库中,前端查询数据库以进行显示。

技术栈组件

  • 前端:React + TypeScript + Tailwind CSS + shadcn/ui & Radix UI
    • 在响应式界面中显示交易数据
    • 每 1000 毫秒轮询数据库以进行更新
    • 通过数据库请求处理钱包切换
  • 后端:Node.js 索引器,轮询间隔为 500 毫秒
    • 从 5 个不同的 Hyperliquid 端点获取数据
    • 使用适当的精度处理将数据存储在 PostgreSQL 中
    • 管理来自前端的钱包切换请求

轮询注意事项

本指南使用积极的轮询间隔(索引器 500 毫秒,前端 1000 毫秒)来演示实时更新。如果需要,你可以调整这些间隔:

  • 前端: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
    • 使用财务精度(DECIMAL 类型)将交易数据存储在 6 个表中
    • 通过 wallet_switch_requests 表处理前端和索引器之间的通信
    • 使用唯一约束来防止重复条目
  • 数据源:通过 QuickNode 的 Hyperliquid info 端点
    • 提供账户数据、仓位、金库存有量、现货余额和委托
    • 以 JSON 格式返回数据,其中字符串数字用于精度
    • 通过带有钱包地址参数的 HTTP POST 请求访问
                    ┌─────────────────┐
                    │   Perp Trader   │
                    └─────────┬───────┘
                              │ 1. Enter wallet address (输入钱包地址)
                              ▼
                    ┌─────────────────┐
                    │ React Dashboard │◄─────────────────┐
                    └─────────┬───────┘                  │
                              │ 2. Store request (存储请求)       │ 6. Read & display data (读取并显示数据)
                              ▼                          │
                    ┌─────────────────┐                  │
                    │    Supabase     │◄─────────────────┤
                    │   PostgreSQL    │                  │
                    └─────────┬───────┘                  │
                              │ 3. Detect request (检测请求)        │ 5. Store data (存储数据)
                              ▼                          │
                    ┌─────────────────┐                  │
                    │     Indexer     │──────────────────┘
                    │   (500ms poll)  │
                    └─────────┬───────┘
                              │ 4. Fetch HyperCore data (获取 HyperCore 数据)
                              ▼
                    ┌─────────────────┐
                    │   QuickNode     │
                    │  Hyperliquid    │
                    │   Endpoint      │
                    └─────────────────┘

应用程序流程

第一次钱包搜索:

  1. 用户输入钱包 → 前端验证并将请求存储在数据库中
  2. 索引器检测到请求(每 500 毫秒轮询)→ 切换到新钱包
  3. 数据收集开始 → 从 5 个给定的端点独立获取数据并将其存储到数据库
  4. 前端轮询数据库(每 1000 毫秒)→ UI 使用从索引器获取的新鲜数据进行更新

第二次及以后的钱包搜索:

  1. 用户输入新钱包 → 前端调用以清除任何先前钱包的数据,并命令索引器切换到新钱包
  2. 索引器检测到新请求 → 立即切换到新钱包,并开始使用新的钱包地址获取数据
  3. 数据实时更新 → 仪表板开始获取新钱包的仓位和指标

项目结构

该项目遵循一个清晰的、模块化的架构,该架构将数据收集、UI 组件和业务逻辑之间的关注点分离开来。这种结构使代码库易于维护,并可以轻松扩展功能。

目录分解

├── src/
│   ├── indexer/
│   │   ├── indexer.ts          # Main indexer orchestration & wallet management (主索引器编排和钱包管理)
│   │   └── apicalls.ts         # Hyperliquid info endpoint queries (Hyperliquid 信息端点查询)
│   ├── components/
│   │   ├── ui/                 # shadcn/ui components (Button, Input, Card, etc.)
│   │   └── dashboard/          # Dashboard components (WalletHeader, PortfolioMetrics, etc.)
│   ├── shared/
│   │   ├── types.ts            # TypeScript interfaces & types (TypeScript 接口和类型)
│   │   ├── utils.ts            # Formatting, calculations & utility functions (格式化、计算和实用功能)
│   │   ├── constants.ts        # UI constants for the dashboard (仪表板的 UI 常量)
│   │   └── supabase.ts         # Supabase client instance for frontend access (用于前端访问的 Supabase 客户端实例)
│   ├── Dashboard.tsx           # Main Dashboard logic (主仪表板逻辑)
│   └── main.tsx
├── supabase/
│   └── schema.sql              # Complete database schema (完整的数据库模式)
├── 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 账户。

创建一个新项目,然后单击“连接”按钮。

数据库连接

在“应用程序框架”部分中,选择 React 并将 using 字段更改为 Vite。复制 VITE_SUPABASE_URLVITE_SUPABASE_ANON_KEY 值并将它们添加到你的 .env 文件中。

数据库选择 React

最后,导航到右上角的 SQL 编辑器,粘贴 schema.sql 文件的内容,然后单击运行。这将创建所有必需的表和函数,我们需要这些表和函数来存储和获取前端的数据。

SQL 编辑器

步骤 4:QuickNode 设置

创建你的免费试用 QuickNode 帐户,然后创建你的第一个 Hyperliquid RPC 端点并将其粘贴到你的 .env 文件中。

info

确保删除现有的 /evm 并在你的 QuickNode 端点 URL 的末尾添加 /info,以获得对 Hyperliquid 信息端点的访问权限。

步骤 5:启动应用程序

一旦创建了表并配置了你的环境,你就可以通过在根目录中运行以下命令来启动项目:

npm install && npm run dev:both

这将同时运行前端应用程序和索引器。

当索引器开始运行时,它将等待你搜索有效的钱包地址:

索引器启动

在 localhost URL 上打开你的前端页面,然后单击演示钱包按钮以获取示例钱包地址:

钱包搜索

搜索钱包后,索引器将开始每 500 毫秒获取该地址的数据:

索引器运行

仪表板将显示钱包的实时统计信息:

仪表板页面

你现在可以实时访问帐户价值、活跃仓位和其他交易数据。

故障排除

在使用索引器时,你可能会在设置或运行时遇到以下问题:

  • 索引器停止响应
  • 钱包搜索后没有出现数据
  • 意外停止了索引器

解决方案:

通过运行以下命令重新启动索引器:

npm run dev:indexer

然后通过输入有效的钱包地址再次尝试搜索。

代码库深入

现在你已经启动并运行了portfolio tracker,让我们来深入探讨它的工作原理。本节将深入探讨使实时投资组合跟踪成为可能的三大核心组件:

索引器- 通过 apicalls.ts 处理 Hyperliquid 的 info 端点,检测钱包切换请求,并每 500 毫秒编排来自 5 个不同 Hyperliquid 端点的数据收集。

模式和数据库- 演示了 PostgreSQL 表结构,这些表结构以财务精度存储交易数据,包括前端-索引器通信的协调机制。

仪表板(前端)- 涵盖 React 状态管理、实时数据库轮询、钱包地址验证以及显示实时交易数据的用户界面。

索引器

索引器作为单独的 Node.js 进程运行,每 500 毫秒轮询 QuickNode 端点。它获取当前钱包地址的交易数据并将其存储在数据库中。索引器等待来自前端的钱包切换请求,并使用锁定文件处理进程隔离。

钱包变更检测

索引器每 500 毫秒轮询 wallet_switch_requests 表,以检测前端钱包切换,使用状态字段以防止切换过程中的竞争条件。

Hyperliquid info 端点集成

索引器使用 apicalls.ts 中的 HyperliquidAPI 类与 QuickNode 的 Hyperliquid 端点进行通信。每个 API 方法都遵循一致的模式来获取不同类型的交易数据:

// From apicalls.ts - Main account data fetching (来自 apicalls.ts - 主要账户数据获取)
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 个不同的 info 端点,并将结果存储在单独的数据库表中:

// From indexer.ts - Core data fetching and storage (来自 indexer.ts - 核心数据获取和存储)
const data = await hyperliquidAPI.getClearinghouseState(CURRENT_WALLET_ADDRESS);
const stateId = await this.storeClearinghouseState(data);

// Store positions with atomic replacement to prevent UI flickering (使用原子替换存储仓位以防止 UI 闪烁)
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 (来自 indexer.ts - 锁定文件创建和进程检查)
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 (来自 schema.sql - 核心交易数据存储)
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') (资产符号(例如,“BTC”、“ETH”、“SOL”))
  size DECIMAL(20, 5) NOT NULL,        -- Position size: 20 digits total, 5 after decimal (仓位大小:总共 20 位数字,小数点后 5 位)
  leverage_type TEXT NOT NULL,         -- 'cross' or 'isolated' margin mode ('cross' 或 'isolated' 保证金模式)
  leverage_value INTEGER NOT NULL,     -- Leverage multiplier (1x, 5x, 10x, etc.) (杠杆倍数(1x、5x、10x 等))

  entry_price DECIMAL(20, 5),          -- Average entry price with 5 decimal precision (平均入场价格,精度为 5 位小数)
  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 (来自 HyperCore 的 Unix 时间戳,以毫秒为单位)
  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) 类型提供 20 个总位数,小数点后 5 位。

钱包切换请求表- 协调前端和索引器之间的通信:

-- From schema.sql - Frontend-indexer coordination (来自 schema.sql - 前端-索引器协调)
CREATE TABLE wallet_switch_requests (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  requested_wallet_address TEXT NOT NULL,           -- Ethereum address (0x format) (以太坊地址(0x 格式))
  status TEXT NOT NULL DEFAULT 'pending',           -- State machine: pending → processing → completed (状态机:pending → processing → completed)
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Index for efficient status-based queries by indexer (索引器用于有效的基于状态的查询的索引)
CREATE INDEX idx_wallet_switch_requests_status ON wallet_switch_requests(status);

此表充当 React 前端和 Node.js 索引器之间的消息队列。当用户输入新的钱包地址时,前端会插入一个 "pending" 请求。索引器每 500 毫秒轮询一次 "pending" 请求,将状态更新为 "processing" 以防止竞争情况,然后执行钱包切换。状态进展确保每次只发生一个钱包切换,即使在用户快速搜索不同的地址时也是如此。

仪表板(前端)

在有了索引器收集数据和数据库存储数据的情况下,最后一部分就是用户界面。React 前端每 1000 毫秒轮询一次数据库以获取更新的交易数据,并使用模块化组件显示它。它通过将请求插入数据库并立即清除本地状态来管理钱包切换。

概述

仪表板架构围绕着中心化状态管理和模块化组件组合:

// From Dashboard.tsx - Centralized state management (来自 Dashboard.tsx - 中心化状态管理)
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);

Dashboard 组件将所有交易数据保存在 React 状态中,并将其作为 props 传递给子组件。这种模式使状态管理变得简单,并使在调试期间跟踪数据流变得容易。

自动刷新轮询

前端使用具有适当清除的 setInterval 循环每 1000 毫秒轮询数据库:

// From Dashboard.tsx - Auto-refresh with cleanup (来自 Dashboard.tsx - 自动刷新并清除)
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 (来自 Dashboard.tsx - 实时数据查询)
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 (来自 Dashboard.tsx - 钱包验证和切换)
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 (来自 PortfolioMetrics.tsx - 投资组合概述卡组件)
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>
  );
};

Dashboard 组件将计算出的值和格式化函数传递给子组件,这些子组件处理交易数据的可视化呈现和样式设置。

结论

恭喜!你已经成功构建了一个使用 QuickNode 的 Hyperliquid 信息端点的实时 Hyperliquid 投资组合跟踪器。你已经学习了如何获取永续合约交易数据、构建用于财务精度的 PostgreSQL 数据库以及构建实时更新的响应式仪表板。这个基础解锁了高级交易工具的可能性,例如自动风险监控和多钱包比较。

你可以通过集成图表库(如 Recharts)或使用你喜欢的通知服务构建交易警报来进一步扩展此项目。查看我们的其他 Hyperliquid 指南,以探索更多在 Hyperliquid 上构建的方法。

下一步

现在你已经有了一个可用的投资组合跟踪器,以下是扩展和改进应用程序的几种方法:

  • 当仓位接近危险保证金水平时发出清算警告
  • 随时间推移跟踪投资组合绩效并生成图表
  • 缩短钱包搜索之间的加载时间

更多资源

如果你遇到困难或有疑问,请在我们的 Discord 中提出。通过在 Twitter (@QuickNode) 或我们的 Telegram 公告频道 上关注我们,及时了解最新信息。

我们 ❤️ 反馈!

如果你对新主题有任何反馈或要求,请告诉我们。我们很乐意听取你的意见。

  • 原文链接: quicknode.com/guides/hyp...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。