Розробка системи обліку позицій фонду

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1306 послуг
Розробка системи обліку позицій фонду
Складний
~1-2 тижні
Часті запитання

Напрямки блокчейн-розробки

Етапи блокчейн-розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1311
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1222
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    922
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1151
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    614
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    887

Розробка системи учету позицій крипто-фонду

Учет позицій крипто-фонду — це не "подивився баланс на etherscan". Це багатовимірна задача: позиції розповсюджені по десяткам гаманців та кількох блокчейнах, значна частина заблокована у DeFi-протоколах, вартість деривативних позицій змінюється нелінійно, і все це потрібно агрегувати у реальному часі з коректним розрахунком P&L та обліком історичної вартості для податкової звітності.

Готові рішення типу Zapper або DeBank добрі для фізичних осіб. Для фонду з кастодіальними вимогами, аудитом та власною методологією розрахунку — потрібна кастомна система.

Архітектура системи

Система складається з чотирьох шарів:

Data Layer:    Blockchain Nodes / RPC Providers
               ↓
Indexing:      Position Fetchers (per protocol)
               ↓
Accounting:    Valuation Engine + P&L Calculator
               ↓
Reporting:     API + Dashboard + Audit Export

Data Layer: джерела даних

Для кожної позиції потрібні різні джерела:

Spot holdings (токени на гаманцях): eth_call до ERC-20 balanceOf або агрегація через Moralis/Alchemy getTokenBalances. Для мультичейну — паралельні запити до кожної цепочки.

DeFi позиції — найскладніша частина. У кожного протоколу своя модель:

Протокол Тип позиції Спосіб отримання
Uniswap V3 LP position (NFT) NonfungiblePositionManager.positions(tokenId)
Aave V3 Lending/borrowing aaveDataProvider.getUserReserveData()
Compound V3 Supply/borrow comet.balanceOf() + comet.borrowBalanceOf()
Curve LP shares pool.balances() + gauge.balanceOf()
GMX Perp positions Reader.getPositions()
Lido stETH stETH.balanceOf() (rebasing!)

Locked/vested позиції: vesting контракти, gauge locks (Curve/Velodrome veNFT), staking з lockup. Ці позиції мають майбутню вартість з часовим дисконтом — потрібно вирішити як їх учитувати в NAV.

Position Fetcher: рівень абстракції

Кожен протокол реалізує інтерфейс PositionFetcher:

interface Position {
    protocol: string
    chain: string
    type: 'spot' | 'lp' | 'lending' | 'borrowing' | 'staking' | 'perp'
    tokens: TokenAmount[]    // складові позиції
    valueUsd: Decimal        // поточна вартість
    metadata: Record<string, unknown>
}

interface PositionFetcher {
    protocol: string
    chains: string[]
    fetch(wallet: string, blockNumber?: number): Promise<Position[]>
}

Приклад Uniswap V3 fetcher:

class UniswapV3Fetcher implements PositionFetcher {
    protocol = 'uniswap-v3'
    chains = ['ethereum', 'arbitrum', 'optimism', 'polygon', 'base']

    async fetch(wallet: string, blockNumber?: number): Promise<Position[]> {
        const nfpm = new ethers.Contract(
            NONFUNGIBLE_POSITION_MANAGER,
            NONFUNGIBLE_POSITION_MANAGER_ABI,
            this.provider
        )
        const overrides = blockNumber ? { blockTag: blockNumber } : {}

        // Отримання всіх LP NFT через Transfer events (від genesis до сейчас)
        const balance = await nfpm.balanceOf(wallet, overrides)
        const tokenIds = await Promise.all(
            Array.from({ length: Number(balance) }, (_, i) =>
                nfpm.tokenOfOwnerByIndex(wallet, i, overrides)
            )
        )

        const positions = await Promise.all(
            tokenIds.map(async (id) => {
                const pos = await nfpm.positions(id, overrides)
                return this.decodePosition(id, pos, wallet, blockNumber)
            })
        )

        return positions.filter(p => p.tokens[0].amount > 0n || p.tokens[1].amount > 0n)
    }

    private async decodePosition(
        tokenId: bigint,
        pos: UniswapV3PositionStruct,
        wallet: string,
        blockNumber?: number
    ): Promise<Position> {
        // Розрахунок amounts з liquidity + tickLower + tickUpper + currentSqrtPriceX96
        const [token0Amount, token1Amount] = getAmountsForLiquidity(
            await this.getCurrentSqrtPrice(pos.poolAddress, blockNumber),
            pos.tickLower,
            pos.tickUpper,
            pos.liquidity
        )

        // Накопилені комісії (unclaimed)
        const [fees0, fees1] = await this.getUnclaimedFees(tokenId, blockNumber)

        return {
            protocol: 'uniswap-v3',
            chain: this.chain,
            type: 'lp',
            tokens: [
                { token: pos.token0, amount: token0Amount + fees0 },
                { token: pos.token1, amount: token1Amount + fees1 },
            ],
            valueUsd: await this.calculateUsdValue(pos.token0, token0Amount, pos.token1, token1Amount),
            metadata: { tokenId: tokenId.toString(), fee: pos.fee, tickRange: [pos.tickLower, pos.tickUpper] },
        }
    }
}

Оцінка вартості (Valuation Engine)

Джерела цін

Для точної оцінки потрібна ієрархія джерел цін:

  1. DEX on-chain price (Uniswap V3 TWAP) — маніпульовано при короткому вікні, надійно при 30-хвилинному TWAP
  2. CEX агрегатор (CoinGecko, CoinMarketCap API) — затримка до 5 хвилин, немає для exotics
  3. Pyth Network / Chainlink — on-chain oracle, для позицій які вже on-chain
  4. Manual pricing — для ілліквідних токенів, OTC позицій
class ValuationEngine {
    private priceCache = new Map<string, { price: Decimal; timestamp: number }>()

    async getPrice(tokenAddress: string, chain: string): Promise<Decimal> {
        const cacheKey = `${chain}:${tokenAddress}`
        const cached = this.priceCache.get(cacheKey)

        // Кеш 60 секунд для активних рыночних годин
        if (cached && Date.now() - cached.timestamp < 60_000) {
            return cached.price
        }

        const price = await this.fetchPriceWithFallback(tokenAddress, chain)
        this.priceCache.set(cacheKey, { price, timestamp: Date.now() })
        return price
    }

    private async fetchPriceWithFallback(token: string, chain: string): Promise<Decimal> {
        // 1. Спроба отримати ціну із Uniswap V3 TWAP (30 хв)
        try {
            return await this.getUniswapTWAP(token, chain, 1800)
        } catch {}

        // 2. CoinGecko API
        try {
            return await this.getCoinGeckoPrice(token, chain)
        } catch {}

        // 3. Остання відома ціна з БД позначена як stale
        const lastKnown = await this.db.getLastKnownPrice(token, chain)
        if (lastKnown) {
            this.emitAlert(`STALE_PRICE: ${token} on ${chain}`)
            return lastKnown.price
        }

        throw new Error(`Cannot price token ${token} on ${chain}`)
    }
}

Rebasing токени

Особливий випадок — rebasing токени: stETH, aUSDC, aETH. Їхній баланс змінюється кожний блок без Transfer подій. Либо враховувати реальний баланс через balanceOf замість Transfer-based accounting, либо конвертувати у wrapped версію (wstETH замість stETH).

P&L розрахунок та cost basis

Для податкової звітності та performance reporting потрібно відслідковувати історичну вартість позицій. Два основних методи:

FIFO (First In, First Out) — для кожної продажі берем вартість найранніше купленої порції. Вимагає повної історії придбань.

Average Cost Basis — середня вартість всіх куплених токенів. Простіше у розрахунку, менш вигідна податково на зростаючому ринку.

class CostBasisTracker {
    // Лоти: кожна покупка — окремий лот з датою та ціною
    async recordAcquisition(
        wallet: string,
        token: string,
        amount: Decimal,
        priceUsd: Decimal,
        txHash: string,
        timestamp: Date
    ): Promise<void> {
        await this.db.query(`
            INSERT INTO cost_basis_lots
                (wallet, token, amount, price_usd, cost_basis_usd, acquired_at, tx_hash)
            VALUES ($1, $2, $3, $4, $3 * $4, $5, $6)
        `, [wallet, token, amount, priceUsd, timestamp, txHash])
    }

    async calculateRealizedPnl(
        wallet: string,
        token: string,
        soldAmount: Decimal,
        soldPriceUsd: Decimal
    ): Promise<{ realizedPnl: Decimal; costBasis: Decimal }> {
        // FIFO: берем лоти в порядку придбання
        const lots = await this.db.query(`
            SELECT id, amount, price_usd
            FROM cost_basis_lots
            WHERE wallet = $1 AND token = $2 AND remaining_amount > 0
            ORDER BY acquired_at ASC
        `, [wallet, token])

        let remaining = soldAmount
        let totalCostBasis = new Decimal(0)

        for (const lot of lots.rows) {
            if (remaining.lte(0)) break
            const consumed = Decimal.min(remaining, new Decimal(lot.remaining_amount))
            totalCostBasis = totalCostBasis.plus(consumed.mul(lot.price_usd))
            remaining = remaining.minus(consumed)
            await this.updateLotRemainder(lot.id, consumed)
        }

        const proceeds = soldAmount.mul(soldPriceUsd)
        return {
            realizedPnl: proceeds.minus(totalCostBasis),
            costBasis: totalCostBasis,
        }
    }
}

Snapshots та історичний NAV

Для аудиту та investor reporting потрібні історичні snapshots. Система має бути здатна відтворити портфель на будь-яку історичну дату:

CREATE TABLE portfolio_snapshots (
    id              BIGSERIAL PRIMARY KEY,
    snapshot_at     TIMESTAMPTZ NOT NULL,
    wallet          VARCHAR(42) NOT NULL,
    block_number    BIGINT NOT NULL,  -- якір до конкретного блоку
    total_value_usd NUMERIC(20, 6) NOT NULL,
    positions       JSONB NOT NULL,   -- серіалізований список позицій
    prices_used     JSONB NOT NULL    -- ціни, використані для оцінки
);

-- Індекс для швидкого пошуку NAV за період
CREATE INDEX idx_snapshots_wallet_time
ON portfolio_snapshots (wallet, snapshot_at DESC);

Historical block snapshot вимагає archive node (або провайдера з archive access). Alchemy та QuickNode надають історичний стан через eth_call з blockTag.

Алерти та операційний мониторинг

Система учету позицій без алертів — це система, яку перестають перевіряти. Обов'язкові тригери:

  • Зміна позиції > N% за 15 хвилин (неочікувана активність)
  • Ціна токена недоступна > 5 хвилин (STALE_PRICE)
  • Розходження між on-chain балансом та обліком > 0.1%
  • Ліквідація позиції у lending протоколі
  • Uniswap V3 позиція вийшла за межі діапазону

Стек: Node.js / Go бекенд, PostgreSQL з TimescaleDB, Redis для кеша цін, Grafana дашборд, PagerDuty для критичних алертів. Archive RPC через Alchemy/QuickNode для історичних даних.