コンテンツにスキップ
このページの翻訳は LLM によって生成されています。誤りに気づいた場合は GitHub で issue を開いてお知らせください。

フィルターとページネーション

絞り込み、ページ分割、並べ替えを使った高度なクエリを学びます。


データ型に応じたフィルターに対応します。

StringFilter は等値・パターン一致に対応します。

input StringFilter {
eq: String # Exact match
like: String # Pattern match
}

例:完全一致

query SearchToken {
oracle {
tokens(where: { name: { eq: "Bitcoin" } }) {
id
symbol
name
}
}
}

例:パターン一致

query SearchByPattern {
oracle {
tokens(where: { name: { like: "%coin%" } }) {
symbol
name
}
}
}

JavaScriptでの使い方:

// Exact match
const { data } = await client.query({
query: gql`
query SearchToken($name: String!) {
oracle {
tokens(where: { name: $name }) {
symbol
name
}
}
}
`,
variables: { name: "Bitcoin" }
})
// Pattern search
const searchResults = await client.query({
query: gql`
query SearchTokens($pattern: String!) {
oracle {
tokens(where: { name: $pattern }) {
symbol
name
}
}
}
`,
variables: { pattern: "%BTC%" }
})

IntFilter は比較演算に対応します。

input IntFilter {
eq: Int # Equal to
gt: Int # Greater than
gte: Int # Greater than or equal
lt: Int # Less than
lte: Int # Less than or equal
}

例:手数料範囲

query GetLargeFees {
fee {
feeTransactions(
filter: {
minAmount: 1000000 # >= 1,000,000
maxAmount: 10000000 # <= 10,000,000
}
) {
id
totalFeeCharged
traderAddress
}
}
}

JavaScriptでの使い方:

const { data } = await client.query({
query: gql`
query GetFeesInRange($min: Int!, $max: Int!) {
fee {
feeTransactions(
filter: {
minAmount: $min
maxAmount: $max
}
) {
id
totalFeeCharged
}
}
}
`,
variables: {
min: 1000000,
max: 10000000
}
})

FloatFilter は浮動小数点の比較に使います。

input FloatFilter {
eq: Float # Equal to
gt: Float # Greater than
gte: Float # Greater than or equal
lt: Float # Less than
lte: Float # Less than or equal
}

例:レバレッジで絞り込み

// Note: Direct float filters not used in current schema,
// but filter objects use similar patterns
const highLeverageTrades = await client.query({
query: gql`
query GetHighLeverageTrades($trader: String!) {
perp {
trades(where: { trader: $trader }) {
id
leverage
isOpen
}
}
}
`,
variables: { trader: "nibi1abc..." }
})
// Filter client-side
const filtered = highLeverageTrades.data.perp.trades.filter(
trade => trade.leverage > 10
)

TimeFilter は日時範囲に使います。

input TimeFilter {
eq: Time # Exact time
gt: Time # After
gte: Time # After or at
lt: Time # Before
lte: Time # Before or at
}

例:期間で絞り込み

query GetFeesLastMonth {
fee {
feeTransactions(
filter: {
fromDate: "2024-10-01T00:00:00Z"
toDate: "2024-10-31T23:59:59Z"
}
limit: 100
) {
id
totalFeeCharged
blockTime
}
}
}

JavaScriptでの使い方:

// Get fees for last 30 days
const thirtyDaysAgo = new Date()
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
const { data } = await client.query({
query: gql`
query GetRecentFees($fromDate: Time!, $toDate: Time!) {
fee {
feeTransactions(
filter: {
fromDate: $fromDate
toDate: $toDate
}
limit: 100
) {
id
totalFeeCharged
blockTime
}
}
}
`,
variables: {
fromDate: thirtyDaysAgo.toISOString(),
toDate: new Date().toISOString()
}
})

補助関数:

function getDateRange(days) {
const toDate = new Date()
const fromDate = new Date()
fromDate.setDate(fromDate.getDate() - days)
return {
fromDate: fromDate.toISOString(),
toDate: toDate.toISOString()
}
}
// Usage
const last7Days = getDateRange(7)
const last30Days = getDateRange(30)
const last90Days = getDateRange(90)

列挙値で絞り込みます。

例:手数料種類

query GetOpeningFees($trader: String!) {
fee {
feeTransactions(
filter: {
traderAddress: $trader
feeType: OPENING
}
limit: 50
) {
id
feeType
totalFeeCharged
}
}
}

例:取引種類

// Get only limit orders
const { data } = await client.query({
query: gql`
query GetLimitOrders($trader: String!) {
perp {
tradeHistory(where: { trader: $trader }, limit: 100) {
id
tradeChangeType
trade {
id
tradeType
}
}
}
}
`,
variables: { trader: "nibi1abc..." }
})
// Filter for limit_order_created events
const limitOrders = data.perp.tradeHistory.filter(
h => h.tradeChangeType === 'limit_order_created'
)

limit と offset でページ分割します。

query GetTradesPaginated($trader: String!, $limit: Int!, $offset: Int!) {
perp {
trades(
where: { trader: $trader }
limit: $limit
offset: $offset
) {
id
isOpen
leverage
}
}
}

JavaScript実装:

async function fetchPage(trader, page = 0, pageSize = 20) {
const { data } = await client.query({
query: GET_TRADES_PAGINATED,
variables: {
trader,
limit: pageSize,
offset: page * pageSize
}
})
return data.perp.trades
}
// Get first page
const firstPage = await fetchPage("nibi1abc...", 0, 20)
// Get second page
const secondPage = await fetchPage("nibi1abc...", 1, 20)
// Get third page
const thirdPage = await fetchPage("nibi1abc...", 2, 20)

複数ページから全件取得します。

async function fetchAllTrades(trader, pageSize = 100) {
let allTrades = []
let page = 0
let hasMore = true
while (hasMore) {
const { data } = await client.query({
query: gql`
query GetTrades($trader: String!, $limit: Int!, $offset: Int!) {
perp {
trades(
where: { trader: $trader }
limit: $limit
offset: $offset
) {
id
isOpen
leverage
}
}
}
`,
variables: {
trader,
limit: pageSize,
offset: page * pageSize
}
})
const trades = data.perp.trades
allTrades = allTrades.concat(trades)
hasMore = trades.length === pageSize
page++
console.log(`Fetched page ${page}, total: ${allTrades.length}`)
}
return allTrades
}
// Usage
const allTrades = await fetchAllTrades("nibi1abc...")
console.log(`Total trades: ${allTrades.length}`)

Keeperはoffset方式ですが、カーソル式の処理も実装できます。

async function fetchTradesAfterBlock(trader, afterBlock = 0, limit = 100) {
const { data } = await client.query({
query: gql`
query GetTrades($trader: String!, $limit: Int!) {
perp {
trades(
where: { trader: $trader }
limit: $limit
order_desc: false
) {
id
openBlock {
block
}
}
}
}
`,
variables: { trader, limit }
})
// Filter trades after cursor block
const filtered = data.perp.trades.filter(
trade => trade.openBlock.block > afterBlock
)
return filtered
}
// Get first batch
const firstBatch = await fetchTradesAfterBlock("nibi1abc...", 0)
const lastBlock = firstBatch[firstBatch.length - 1].openBlock.block
// Get next batch
const nextBatch = await fetchTradesAfterBlock("nibi1abc...", lastBlock)

UIに無限スクロールを実装します。

class TradesPaginator {
constructor(client, trader, pageSize = 20) {
this.client = client
this.trader = trader
this.pageSize = pageSize
this.currentPage = 0
this.allTrades = []
this.hasMore = true
}
async loadMore() {
if (!this.hasMore) {
return []
}
const { data } = await this.client.query({
query: gql`
query GetTrades($trader: String!, $limit: Int!, $offset: Int!) {
perp {
trades(
where: { trader: $trader }
limit: $limit
offset: $offset
order_desc: true
) {
id
isOpen
leverage
}
}
}
`,
variables: {
trader: this.trader,
limit: this.pageSize,
offset: this.currentPage * this.pageSize
}
})
const trades = data.perp.trades
this.allTrades = this.allTrades.concat(trades)
this.hasMore = trades.length === this.pageSize
this.currentPage++
return trades
}
reset() {
this.currentPage = 0
this.allTrades = []
this.hasMore = true
}
getAll() {
return this.allTrades
}
}
// Usage
const paginator = new TradesPaginator(client, "nibi1abc...", 20)
// Load first page
const firstPage = await paginator.loadMore()
// Load more on scroll
window.addEventListener('scroll', async () => {
if (isNearBottom() && paginator.hasMore) {
const nextPage = await paginator.loadMore()
appendToUI(nextPage)
}
})

多くのクエリは order_by と order_desc に対応します。

query GetTradesOrdered($trader: String!) {
perp {
trades(
where: { trader: $trader }
order_by: sequence
order_desc: true # Descending (newest first)
) {
id
openBlock { block_ts }
}
}
}

領域別の並べ替え項目:

無期限先物取引:

  • sequence:連番で並べ替え。

取引履歴:

  • sequence:連番。
  • trade_id:取引ID。

LP入金:

  • depositor:預金者アドレス。
  • vault:ボールトアドレス。

LP入金履歴:

  • depositor:預金者。
  • sequence:連番。
  • vault:ボールト。

LP出金申請:

  • depositor:預金者。
  • unlock_epoch:解除エポック。
  • vault:ボールト。

オラクルトークン:

  • id:トークンID。
  • name:名前。
  • permission_group:権限グループ。

手数料取引:

  • 明示的な並べ替えパラメーターはなく、ID順です。

新しい順:

const { data } = await client.query({
query: gql`
query GetRecentTrades($trader: String!) {
perp {
trades(
where: { trader: $trader }
order_desc: true
limit: 20
) {
id
openBlock { block_ts }
}
}
}
`,
variables: { trader: "nibi1abc..." }
})

古い順:

const { data } = await client.query({
query: gql`
query GetOldestTrades($trader: String!) {
perp {
trades(
where: { trader: $trader }
order_desc: false
limit: 20
) {
id
openBlock { block_ts }
}
}
}
`,
variables: { trader: "nibi1abc..." }
})

トークン名:

const { data } = await client.query({
query: gql`
query GetTokensAlphabetically {
oracle {
tokens(
order_by: name
order_desc: false
) {
symbol
name
}
}
}
`
})

複数フィルターで正確に絞り込みます。

const { data } = await client.query({
query: gql`
query GetFilteredFees(
$trader: String!
$feeType: FeeType!
$fromDate: Time!
$toDate: Time!
$minAmount: Int!
) {
fee {
feeTransactions(
filter: {
traderAddress: $trader
feeType: $feeType
fromDate: $fromDate
toDate: $toDate
minAmount: $minAmount
}
limit: 100
) {
id
totalFeeCharged
blockTime
}
}
}
`,
variables: {
trader: "nibi1abc...",
feeType: "OPENING",
fromDate: "2024-01-01T00:00:00Z",
toDate: "2024-12-31T23:59:59Z",
minAmount: 1000000
}
})

クライアント側の追加絞り込み

Section titled “クライアント側の追加絞り込み”

サーバーで絞り込めない場合はクライアントで行います。

// Fetch all trades
const { data } = await client.query({
query: gql`
query GetAllTrades($trader: String!) {
perp {
trades(where: { trader: $trader }, limit: 1000) {
id
leverage
isLong
isOpen
collateralAmount
state {
pnlPct
}
perpBorrowing {
baseToken { symbol }
}
}
}
}
`,
variables: { trader: "nibi1abc..." }
})
// Filter for high-leverage profitable longs
const filtered = data.perp.trades.filter(trade =>
trade.isLong &&
trade.leverage > 10 &&
trade.state.pnlPct > 0.1 && // 10% profit
trade.isOpen
)
// Group by token
const byToken = filtered.reduce((acc, trade) => {
const symbol = trade.perpBorrowing.baseToken.symbol
if (!acc[symbol]) acc[symbol] = []
acc[symbol].push(trade)
return acc
}, {})
console.log('Profitable high-leverage longs by token:', byToken)

検索機能を実装します。

async function searchTrades(trader, searchTerm) {
// Fetch all trades
const { data } = await client.query({
query: gql`
query GetTrades($trader: String!) {
perp {
trades(where: { trader: $trader }) {
id
isLong
leverage
perpBorrowing {
baseToken {
symbol
name
}
marketId
}
}
}
}
`,
variables: { trader }
})
// Search by token symbol or name
const searchLower = searchTerm.toLowerCase()
return data.perp.trades.filter(trade => {
const symbol = trade.perpBorrowing.baseToken.symbol.toLowerCase()
const name = trade.perpBorrowing.baseToken.name.toLowerCase()
return symbol.includes(searchLower) || name.includes(searchLower)
})
}
// Usage
const btcTrades = await searchTrades("nibi1abc...", "bitcoin")
const ethTrades = await searchTrades("nibi1abc...", "eth")

// Good: Reasonable page size
const PAGE_SIZE = 50
// Bad: Too large, slow queries
const PAGE_SIZE = 10000
// Bad: Too small, too many requests
const PAGE_SIZE = 5

推奨件数:

  • 取引:20〜100件。
  • 手数料取引:50〜100件。
  • 履歴:50〜200件。
  • トークン:100〜500件。

class CachedPaginator {
constructor(client, query, variables) {
this.client = client
this.query = query
this.variables = variables
this.cache = new Map()
}
async getPage(page, pageSize = 20) {
const cacheKey = `${page}-${pageSize}`
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey)
}
const { data } = await this.client.query({
query: this.query,
variables: {
...this.variables,
limit: pageSize,
offset: page * pageSize
}
})
this.cache.set(cacheKey, data)
return data
}
clearCache() {
this.cache.clear()
}
}

async function fetchTradesWithEmptyCheck(trader, page = 0) {
const { data } = await client.query({
query: GET_TRADES,
variables: {
trader,
limit: 20,
offset: page * 20
}
})
const trades = data.perp.trades
if (trades.length === 0) {
if (page === 0) {
console.log('No trades found for this trader')
} else {
console.log('No more trades to load')
}
}
return trades
}

import { debounce } from 'lodash'
const debouncedSearch = debounce(async (searchTerm) => {
const results = await searchTrades(trader, searchTerm)
updateUI(results)
}, 300) // Wait 300ms after user stops typing
// Usage in React
function SearchInput() {
const handleChange = (e) => {
debouncedSearch(e.target.value)
}
return <input onChange={handleChange} />
}

async function loadTradesWithUI(trader, page) {
try {
showLoadingSpinner()
const trades = await fetchPage(trader, page)
if (trades.length === 0) {
showEmptyState()
} else {
renderTrades(trades)
}
} catch (error) {
showErrorState(error)
} finally {
hideLoadingSpinner()
}
}

function validateDateRange(fromDate, toDate) {
const from = new Date(fromDate)
const to = new Date(toDate)
if (isNaN(from.getTime()) || isNaN(to.getTime())) {
throw new Error('Invalid date format')
}
if (from > to) {
throw new Error('From date must be before to date')
}
const maxRange = 90 * 24 * 60 * 60 * 1000 // 90 days
if (to - from > maxRange) {
throw new Error('Date range too large (max 90 days)')
}
return { fromDate, toDate }
}
// Usage
try {
const { fromDate, toDate } = validateDateRange(
userInputFrom,
userInputTo
)
const { data } = await client.query({
query: GET_FEES,
variables: { fromDate, toDate }
})
} catch (error) {
showError(error.message)
}