以太坊生態數據儀表板建構完整指南:TVL 趨勢圖、Gas 費用走勢圖與即時監控實戰

本指南深入介紹如何建構專業的以太坊生態數據儀表板,涵蓋 TVL 趨勢圖、Gas 費用歷史走勢圖等視覺化內容的完整技術實作。我們提供多個數據來源的 API 整合方案、Chart.js 視覺化範例、即時監控與自動化警報系統,幫助開發者和投資者即時掌握以太坊生態動態。

以太坊生態數據儀表板建構完整指南:TVL 趨勢圖、Gas 費用走勢圖與即時監控實戰

概述

以太坊生態系統的健康狀況和發展趨勢需要透過多元化的數據指標來衡量。總鎖定價值(TVL)反映了 DeFi 協議的資金流入流出動態,Gas 費用揭示了網路擁堵程度和用戶需求變化,而驗證者數據則呈現網路的去中心化程度和安全狀態。建立完善的生態數據儀表板,可以幫助開發者、投資者和研究者即時掌握以太坊生態的脈動,做出更明智的決策。

本指南將從工程師視角出發,詳細介紹如何建構一個專業的以太坊生態數據儀表板。我們將涵蓋 TVL 趨勢圖的數據來源與繪製方法、Gas 費用歷史走勢圖的即時更新機制、Layer 2 數據整合、以及自訂報表的自動化生成。每個模組都會提供完整的程式碼範例,讀者可以直接複製使用或根據自身需求進行修改。

本指南的目標讀者包括:以太坊開發者需要監控協議關鍵指標、DeFi 投資者希望追蹤資金流向與收益變化、區塊鏈研究者需要收集歷史數據進行分析、以及生態系統參與者想即時掌握網路健康狀況。

第一章:數據來源與 API 整合

1.1 主流數據 API 服務比較

建構以太坊生態數據儀表板的第一步是確定數據來源。目前市場上有多種區塊鏈數據 API 服務,各有其優勢和適用場景。

主流 API 服務詳細比較

服務商免費額度付費方案起始價特色功能數據延遲
Etherscan5 calls/sec$99/月合約驗證、 代幣追蹤即時
Dune Analytics免費查詢$420/月SQL 查詢、可視化即時
DeBank1000 calls/日免費DeFi 投資組合追蹤即時
DeFi Llama開放 API企業版TVL 聚合即時
The Graph自託管免費自訂子圖即時
Alchemy免費層$49/月節點服務、追蹤即時
Infura免費層$95/月節點服務即時
Covalent免費層$299/月歷史數據豐富即時

選擇建議:對於個人開發者和小型項目,推薦使用 DeFi Llama 的開放 API 獲取 TVL 數據,使用 Etherscan API 獲取鏈上交易數據。對於需要深度分析的企業級應用,則建議採用 Dune Analytics 或 The Graph 的自訂子圖方案。

1.2 DeFi Llama TVL API 整合

DeFi Llama 是目前最完整的 DeFi 協議 TVL 聚合平台,涵蓋超過 3000 個協議的數據。以下是完整的 API 整合程式碼:

// ethereum-dashboard/data-sources/defi-llama.js
const axios = require('axios');

class DeFiLlamaAPI {
    constructor() {
        this.baseUrl = 'https://api.llama.fi';
        this.cache = new Map();
        this.cacheTTL = 5 * 60 * 1000; // 5 分鐘快取
    }

    // 獲取以太坊總 TVL 歷史數據
    async getHistoricalTVL(chain = 'Ethereum', days = 365) {
        const cacheKey = `tvl_${chain}_${days}`;
        
        if (this.cache.has(cacheKey)) {
            const cached = this.cache.get(cacheKey);
            if (Date.now() - cached.timestamp < this.cacheTTL) {
                return cached.data;
            }
        }

        try {
            const response = await axios.get(
                `${this.baseUrl}/charts/${chain}`
            );
            
            const data = response.data.map(item => ({
                timestamp: item.date,
                date: new Date(item.date * 1000).toISOString().split('T')[0],
                tvl: item.totalLiquidityUSD,
                tvlChange24h: item.dailyVolumeUSD ? 
                    ((item.totalLiquidityUSD - (item.totalLiquidityUSD - item.dailyVolumeUSD)) / 
                    (item.totalLiquidityUSD - item.dailyVolumeUSD) * 100) : 0
            }));

            this.cache.set(cacheKey, {
                timestamp: Date.now(),
                data: data.slice(-days)
            });

            return data.slice(-days);
        } catch (error) {
            console.error('DeFi Llama API Error:', error.message);
            return [];
        }
    }

    // 獲取特定協議的 TVL
    async getProtocolTVL(protocol, days = 30) {
        try {
            const response = await axios.get(
                `${this.baseUrl}/protocol/${protocol}`
            );
            
            return response.data.tvl?.map(item => ({
                timestamp: item.date,
                date: new Date(item.date * 1000).toISOString().split('T')[0],
                tvl: item.totalLiquidityUSD
            })).slice(-days) || [];
        } catch (error) {
            console.error(`Protocol ${protocol} Error:`, error.message);
            return [];
        }
    }

    // 獲取多鏈 TVL 對比數據
    async getMultiChainTVL() {
        try {
            const response = await axios.get(`${this.baseUrl}/chains`);
            
            return response.data
                .filter(chain => chain.tvl > 100000000) // TVL > $100M
                .map(chain => ({
                    name: chain.name,
                    tvl: chain.tvl,
                    tvlChange24h: chain.change1d,
                    tvlChange7d: chain.change7d,
                    category: chain.category
                }))
                .sort((a, b) => b.tvl - a.tvl);
        } catch (error) {
            console.error('Multi-chain API Error:', error.message);
            return [];
        }
    }

    // 獲取以太坊 Top DeFi 協議 TVL
    async getEthereumTopProtocols(limit = 20) {
        try {
            const response = await axios.get(
                `${this.baseUrl}/overview/DeFi`
            );
            
            const ethereum = response.data.protocols
                .filter(p => p.chain === 'Ethereum')
                .slice(0, limit)
                .map(p => ({
                    name: p.name,
                    slug: p.slug,
                    tvl: p.tvl,
                    change24h: p.change_1d,
                    change7d: p.change_7d,
                    category: p.category
                }));

            return ethereum;
        } catch (error) {
            console.error('Top Protocols Error:', error.message);
            return [];
        }
    }
}

module.exports = DeFiLlamaAPI;

1.3 Etherscan Gas 費用 API 整合

Etherscan 提供了豐富的 Gas 費用數據 API,可以獲取歷史費用和即時報價:

// ethereum-dashboard/data-sources/etherscan.js
const axios = require('axios');

class EtherscanAPI {
    constructor(apiKey) {
        this.apiKey = apiKey || 'YOUR_API_KEY';
        this.baseUrl = 'https://api.etherscan.io/api';
        this.proxyUrl = 'https://api.etherscan.io/api';
    }

    // 獲取以太坊區塊難度與 Gas 限制
    async getBlockData(startBlock, endBlock) {
        const params = {
            module: 'block',
            action: 'getblocknobytime',
            timestamp: Math.floor(Date.now() / 1000) - 86400,
            closest: 'before',
            apikey: this.apiKey
        };

        try {
            const response = await axios.get(this.baseUrl, { params });
            return response.data;
        } catch (error) {
            console.error('Etherscan API Error:', error.message);
            return null;
        }
    }

    // 獲取歷史 Gas 價格
    async getHistoricalGasPrice(days = 30) {
        const results = [];
        const now = Math.floor(Date.now() / 1000);
        const daySeconds = 86400;

        for (let i = 0; i < days; i++) {
            const timestamp = now - (i * daySeconds);
            
            try {
                // 嘗試從多個來源獲取歷史數據
                const response = await axios.get(
                    `https://api.etherscan.io/api?module=block&action=blocknobytime&timestamp=${timestamp}&closest=before&apikey=${this.apiKey}`
                );
                
                if (response.data.status === '1') {
                    const blockNumber = parseInt(response.data.result);
                    
                    results.push({
                        timestamp: timestamp,
                        date: new Date(timestamp * 1000).toISOString().split('T')[0],
                        blockNumber: blockNumber
                    });
                }
            } catch (error) {
                console.error(`Gas data fetch error for day ${i}:`, error.message);
            }
        }

        return results;
    }

    // 估算交易費用
    async estimateTransactionCost(gasLimit, gasPrice) {
        const gasUsed = gasLimit;
        const costWei = BigInt(gasUsed) * BigInt(gasPrice);
        const costEth = Number(costWei) / 1e18;
        
        return {
            gasUsed: gasUsed,
            gasPrice: gasPrice,
            costWei: costWei.toString(),
            costEth: costEth,
            costUSD: costEth * 3000 // 假設 ETH 價格為 $3000
        };
    }
}

// 即時 Gas 價格監控
class GasPriceMonitor {
    constructor(callback) {
        this.callback = callback;
        this.interval = null;
        this.prices = {
            slow: 0,
            standard: 0,
            fast: 0
        };
    }

    async fetchGasPrices() {
        try {
            const response = await axios.get(
                'https://api.etherscan.io/api?module=gastracker&action=gasoracle&apikey=YourApiKeyToken'
            );
            
            if (response.data.status === '1') {
                const result = response.data.result;
                this.prices = {
                    slow: parseInt(result.SafeGasPrice),
                    standard: parseInt(result.ProposeGasPrice),
                    fast: parseInt(result.FastGasPrice),
                    baseFee: parseInt(result.suggestBaseFee),
                    avgGasPrice: parseInt(result.gasPrice)
                };
                
                if (this.callback) {
                    this.callback(this.prices);
                }
                
                return this.prices;
            }
        } catch (error) {
            console.error('Gas price fetch error:', error.message);
        }
        
        return null;
    }

    start(intervalMs = 30000) {
        this.fetchGasPrices();
        this.interval = setInterval(() => this.fetchGasPrices(), intervalMs);
    }

    stop() {
        if (this.interval) {
            clearInterval(this.interval);
            this.interval = null;
        }
    }
}

module.exports = { EtherscanAPI, GasPriceMonitor };

1.4 The Graph 子圖查詢

The Graph 提供了強大的圖形數據查詢能力,適用於複雜的 DeFi 數據分析:

// ethereum-dashboard/data-sources/the-graph.js
const axios = require('axios');

class TheGraphClient {
    constructor(subgraphUrl) {
        this.subgraphUrl = subgraphUrl;
    }

    async query(queryString, variables = {}) {
        try {
            const response = await axios.post(this.subgraphUrl, {
                query: queryString,
                variables: variables
            });
            
            return response.data.data;
        } catch (error) {
            console.error('The Graph Query Error:', error.message);
            return null;
        }
    }

    // 查詢 Uniswap V3 TVL 歷史
    async getUniswapV3TVLHistory(days = 30) {
        const query = `
            query($days: Int!) {
                factories(
                    first: 1,
                    where: { id: "0x1F98431c8aD98523631AE4a59f267346ea31F984" }
                ) {
                    id
                    totalValueLockedUSD
                    poolCount
                    txCount
                    volumeUSD
                }
            }
        `;

        return this.query(query, { days });
    }

    // 查詢 Aave 借貸數據
    async getAaveMarketData() {
        const query = `
            query {
                reserves {
                    symbol
                    name
                    totalDepositsUSD
                    totalBorrowsUSD
                    liquidityRate
                    variableBorrowRate
                    stableBorrowRate
                    utilizationRate
                }
            }
        `;

        return this.query(query);
    }

    // 查詢特定地址的交易歷史
    async getAccountTransactions(address, first = 100) {
        const query = `
            query($address: String!, $first: Int!) {
                swaps(
                    where: { 
                        or: [
                            { from: $address }
                            { to: $address }
                        ]
                    },
                    first: $first,
                    orderBy: timestamp,
                    orderDirection: desc
                ) {
                    id
                    timestamp
                    pair {
                        token0 {
                            symbol
                        }
                        token1 {
                            symbol
                        }
                    }
                    amount0In
                    amount0Out
                    amount1In
                    amount1Out
                    amountUSD
                }
            }
        `;

        return this.query(query, { address, first });
    }
}

// 預設的 The Graph 端點
const SUBGRAPH_ENDPOINTS = {
    uniswapV3: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3',
    aaveV3: 'https://api.thegraph.com/subgraphs/name/aave/aave-v3',
    compound: 'https://api.thegraph.com/subgraphs/name/compound-finance/compound-v3',
    curve: 'https://api.thegraph.com/subgraphs/name/curvefi/curve-finance'
};

module.exports = { TheGraphClient, SUBGRAPH_ENDPOINTS };

第二章:TVL 趨勢圖建構

2.1 TVL 趨勢圖數據處理

TVL(Total Value Locked,總鎖定價值)是衡量 DeFi 協議規模的關鍵指標。一個完整的 TVL 趨勢圖需要處理來自多個數據源的歷史數據,並進行必要的清洗和轉換。

// ethereum-dashboard/charts/tvl-trend.js
const DeFiLlamaAPI = require('../data-sources/defi-llama');

class TVLTrendProcessor {
    constructor()llama = new DeFiLlamaAPI();
    }

 {
        this.    // 處理 TVL 趨勢數據
    async processTVLTrend(chain = 'Ethereum', days = 365) {
        const rawData = await this.llama.getHistoricalTVL(chain, days);
        
        if (!rawData || rawData.length === 0) {
            return null;
        }

        // 計算移動平均線
        const ma7 = this.calculateMA(rawData.map(d => d.tvl), 7);
        const ma30 = this.calculateMA(rawData.map(d => d.tvl), 30);

        // 計算日環比變化
        const dailyChanges = this.calculateDailyChanges(rawData);

        // 計算週環比變化
        const weeklyChanges = this.calculateWeeklyChanges(rawData);

        return rawData.map((item, index) => ({
            date: item.date,
            timestamp: item.timestamp,
            tvl: item.tvl,
            tvlFormatted: this.formatTVL(item.tvl),
            tvlChange24h: dailyChanges[index] || 0,
            tvlChange7d: weeklyChanges[index] || 0,
            ma7: ma7[index] || null,
            ma30: ma30[index] || null,
            ma7Formatted: ma7[index] ? this.formatTVL(ma7[index]) : null,
            ma30Formatted: ma30[index] ? this.formatTVL(ma30[index]) : null
        }));
    }

    // 計算移動平均
    calculateMA(data, period) {
        const result = [];
        for (let i = 0; i < data.length; i++) {
            if (i < period - 1) {
                result.push(null);
            } else {
                const sum = data.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
                result.push(sum / period);
            }
        }
        return result;
    }

    // 計算日環比變化
    calculateDailyChanges(data) {
        return data.map((item, index) => {
            if (index === 0) return 0;
            const prev = data[index - 1].tvl;
            if (prev === 0) return 0;
            return ((item.tvl - prev) / prev) * 100;
        });
    }

    // 計算週環比變化
    calculateWeeklyChanges(data) {
        return data.map((item, index) => {
            if (index < 7) return 0;
            const prev = data[index - 7].tvl;
            if (prev === 0) return 0;
            return ((item.tvl - prev) / prev) * 100;
        });
    }

    // 格式化 TVL 數值
    formatTVL(value) {
        if (value >= 1e12) {
            return `$${(value / 1e12).toFixed(2)}T`;
        } else if (value >= 1e9) {
            return `$${(value / 1e9).toFixed(2)}B`;
        } else if (value >= 1e6) {
            return `$${(value / 1e6).toFixed(2)}M`;
        } else if (value >= 1e3) {
            return `$${(value / 1e3).toFixed(2)}K`;
        }
        return `$${value.toFixed(2)}`;
    }

    // 獲取協議 TVL 排名變化
    async getProtocolRankingChanges(days = 30) {
        const current = await this.llama.getEthereumTopProtocols(50);
        
        // 模擬歷史數據(實際應從數據庫獲取)
        const previous = current.map(p => ({
            ...p,
            tvl: p.tvl / (1 + (p.change7d || 0) / 100)
        }));

        return current.map((protocol, index) => {
            const prevIndex = previous.findIndex(p => p.slug === protocol.slug);
            const rankChange = prevIndex !== -1 ? prevIndex - index : null;
            
            return {
                ...protocol,
                rank: index + 1,
                rankChange: rankChange,
                tvlChangeFormatted: this.formatTVL(protocol.tvl - (previous[index]?.tvl || 0))
            };
        });
    }
}

module.exports = TVLTrendProcessor;

2.2 使用 Chart.js 繪製 TVL 趨勢圖

以下是一個完整的 TVL 趨勢圖繪製範例,使用 Chart.js 作為視覺化庫:

<!-- tvl-dashboard.html -->
<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device=1.0-width, initial-scale">
    <title>以太坊 TVL 趨勢儀表板</title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            margin: 0;
            padding: 20px;
            background: #0d1117;
            color: #c9d1d9;
        }
        .dashboard-container {
            max-width: 1400px;
            margin: 0 auto;
        }
        .header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 24px;
        }
        .tvl-display {
            font-size: 48px;
            font-weight: bold;
            color: #58a6ff;
        }
        .tvl-change {
            font-size: 18px;
            margin-left: 12px;
        }
        .positive { color: #3fb950; }
        .negative { color: #f85149; }
        .chart-container {
            background: #161b22;
            border-radius: 8px;
            padding: 20px;
            margin-bottom: 20px;
        }
        .stats-grid {
            display: grid;
            grid-template-columns: repeat(4, 1fr);
            gap: 16px;
            margin-bottom: 20px;
        }
        .stat-card {
            background: #161b22;
            border-radius: 8px;
            padding: 16px;
        }
        .stat-label {
            font-size: 12px;
            color: #8b949e;
            margin-bottom: 4px;
        }
        .stat-value {
            font-size: 24px;
            font-weight: bold;
        }
    </style>
</head>
<body>
    <div class="dashboard-container">
        <div class="header">
            <div>
                <h1>以太坊生態 TVL 趨勢</h1>
                <div class="tvl-display" id="currentTVL">
                    $0
                    <span class="tvl-change" id="tvlChange">0%</span>
                </div>
            </div>
            <select id="timeRange" onchange="updateTimeRange()">
                <option value="30">最近 30 天</option>
                <option value="90">最近 90 天</option>
                <option value="180">最近 180 天</option>
                <option value="365" selected>最近 365 天</option>
            </select>
        </div>

        <div class="stats-grid">
            <div class="stat-card">
                <div class="stat-label">平均 TVL</div>
                <div class="stat-value" id="avgTVL">$0</div>
            </div>
            <div class="stat-card">
                <div class="stat-label">最高 TVL</div>
                <div class="stat-value" id="maxTVL">$0</div>
            </div>
            <div class="stat-card">
                <div class="stat-label">最低 TVL</div>
                <div class="stat-value" id="minTVL">$0</div>
            </div>
            <div class="stat-card">
                <div class="stat-label">TVL 標準差</div>
                <div class="stat-value" id="stdTVL">$0</div>
            </div>
        </div>

        <div class="chart-container">
            <canvas id="tvlChart"></canvas>
        </div>
    </div>

    <script>
        // TVL 格式化函數
        function formatTVL(value) {
            if (value >= 1e12) return '$' + (value / 1e12).toFixed(2) + 'T';
            if (value >= 1e9) return '$' + (value / 1e9).toFixed(2) + 'B';
            if (value >= 1e6) return '$' + (value / 1e6).toFixed(2) + 'M';
            return '$' + value.toFixed(0);
        }

        // 初始化圖表
        let tvlChart = null;

        async function fetchTVLData(days) {
            // 從 DeFi Llama API 獲取數據
            const response = await fetch(
                `https://api.llama.fi/charts/Ethereum`
            );
            const data = await response.json();
            
            // 過濾指定天數的數據
            const now = Date.now() / 1000;
            const cutoff = now - (days * 86400);
            return data.filter(item => item.date >= cutoff);
        }

        async function renderChart(data) {
            const ctx = document.getElementById('tvlChart').getContext('2d');
            
            const labels = data.map(item => 
                new Date(item.date * 1000).toLocaleDateString('zh-TW')
            );
            const tvlData = data.map(item => item.totalLiquidityUSD);

            // 計算移動平均
            const ma7 = calculateMA(tvlData, 7);
            const ma30 = calculateMA(tvlData, 30);

            if (tvlChart) {
                tvlChart.destroy();
            }

            tvlChart = new Chart(ctx, {
                type: 'line',
                data: {
                    labels: labels,
                    datasets: [
                        {
                            label: 'TVL',
                            data: tvlData,
                            borderColor: '#58a6ff',
                            backgroundColor: 'rgba(88, 166, 255, 0.1)',
                            fill: true,
                            tension: 0.4,
                            pointRadius: 0,
                            pointHoverRadius: 6,
                            borderWidth: 2
                        },
                        {
                            label: '7日均線',
                            data: ma7,
                            borderColor: '#f0883e',
                            borderWidth: 1,
                            pointRadius: 0,
                            tension: 0.4
                        },
                        {
                            label: '30日均線',
                            data: ma30,
                            borderColor: '#a371f7',
                            borderWidth: 1,
                            pointRadius: 0,
                            tension: 0.4
                        }
                    ]
                },
                options: {
                    responsive: true,
                    maintainAspectRatio: false,
                    interaction: {
                        intersect: false,
                        mode: 'index'
                    },
                    plugins: {
                        legend: {
                            labels: {
                                color: '#c9d1d9'
                            }
                        },
                        tooltip: {
                            backgroundColor: '#161b22',
                            titleColor: '#c9d1d9',
                            bodyColor: '#c9d1d9',
                            borderColor: '#30363d',
                            borderWidth: 1,
                            callbacks: {
                                label: function(context) {
                                    return context.dataset.label + ': ' + 
                                        formatTVL(context.raw);
                                }
                            }
                        }
                    },
                    scales: {
                        x: {
                            grid: {
                                color: '#30363d'
                            },
                            ticks: {
                                color: '#8b949e',
                                maxTicksLimit: 12
                            }
                        },
                        y: {
                            grid: {
                                color: '#30363d'
                            },
                            ticks: {
                                color: '#8b949e',
                                callback: function(value) {
                                    return formatTVL(value);
                                }
                            }
                        }
                    }
                }
            });
        }

        function calculateMA(data, period) {
            const result = [];
            for (let i = 0; i < data.length; i++) {
                if (i < period - 1) {
                    result.push(null);
                } else {
                    const sum = data.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
                    result.push(sum / period);
                }
            }
            return result;
        }

        function updateStats(data) {
            const tvlValues = data.map(d => d.totalLiquidityUSD);
            const currentTVL = tvlValues[tvlValues.length - 1];
            const previousTVL = tvlValues[0];
            const change = ((currentTVL - previousTVL) / previousTVL * 100).toFixed(2);

            // 更新當前 TVL
            document.getElementById('currentTVL').innerHTML = 
                formatTVL(currentTVL) + 
                `<span class="tvl-change ${change >= 0 ? 'positive' : 'negative'}">${change}%</span>`;

            // 計算統計數據
            const avg = tvlValues.reduce((a, b) => a + b, 0) / tvlValues.length;
            const max = Math.max(...tvlValues);
            const min = Math.min(...tvlValues);
            const std = Math.sqrt(
                tvlValues.reduce((sq, n) => sq + Math.pow(n - avg, 2), 0) / tvlValues.length
            );

            document.getElementById('avgTVL').textContent = formatTVL(avg);
            document.getElementById('maxTVL').textContent = formatTVL(max);
            document.getElementById('minTVL').textContent = formatTVL(min);
            document.getElementById('stdTVL').textContent = formatTVL(std);
        }

        async function updateTimeRange() {
            const days = parseInt(document.getElementById('timeRange').value);
            const data = await fetchTVLData(days);
            await renderChart(data);
            updateStats(data);
        }

        // 初始化
        updateTimeRange();

        // 自動刷新(每 5 分鐘)
        setInterval(() => updateTimeRange(), 5 * 60 * 1000);
    </script>
</body>
</html>

2.3 TVL 趨勢圖的專業級呈現

對於專業級的 TVL 儀表板,還需要考慮以下進階功能:

// ethereum-dashboard/charts/tvl-advanced.js
class TVLAdvancedAnalyzer {
    constructor() {
        this.llama = new DeFiLlamaAPI();
    }

    // 計算趨勢強度指標
    calculateTrendStrength(data) {
        if (data.length < 14) return null;
        
        const recent = data.slice(-14);
        const previous = data.slice(-28, -14);
        
        const recentAvg = recent.reduce((a, b) => a + b.tvl, 0) / recent.length;
        const previousAvg = previous.reduce((a, b) => a + b.tvl, 0) / previous.length;
        
        const change = (recentAvg - previousAvg) / previousAvg * 100;
        
        return {
            trend: change > 5 ? 'strong_up' : change > 0 ? 'weak_up' : 
                   change < -5 ? 'strong_down' : 'weak_down',
            changePercent: change.toFixed(2),
            interpretation: this.interpretTrend(change)
        };
    }

    interpretTrend(change) {
        if (change > 10) return '強勁上升趨勢';
        if (change > 5) return '溫和上升趨勢';
        if (change > 0) return '輕微上升趨勢';
        if (change > -5) return '輕微下降趨勢';
        if (change > -10) return '溫和下降趨勢';
        return '顯著下降趨勢';
    }

    // 識別 TVL 異常
    async detectAnomalies(data, threshold = 2) {
        const tvlValues = data.map(d => d.tvl);
        const mean = tvlValues.reduce((a, b) => a + b, 0) / tvlValues.length;
        const std = Math.sqrt(
            tvlValues.reduce((sq, n) => sq + Math.pow(n - mean, 2), 0) / tvlValues.length
        );

        const anomalies = [];
        
        for (let i = 1; i < data.length; i++) {
            const zScore = Math.abs((data[i].tvl - mean) / std);
            
            if (zScore > threshold) {
                anomalies.push({
                    date: data[i].date,
                    tvl: data[i].tvl,
                    zScore: zScore.toFixed(2),
                    type: data[i].tvl > mean ? 'surge' : 'drop',
                    possibleCause: this.suggestCause(zScore, data[i].tvl > mean)
                });
            }
        }

        return anomalies;
    }

    suggestCause(isPositive, magnitude) {
        if (isPositive) {
            if (magnitude > 3) return '可能原因:重大協議升級、機構採用、牛市來臨';
            return '可能原因:新協議上線、獎勵活動、TVL 移入';
        } else {
            if (magnitude > 3) return '可能原因:市場崩盤、黑天鵝事件、協議被攻擊';
            return '可能原因:獲利了結、協議 TVs下降、負面新聞';
        }
    }

    // TVL 預測(簡單線性回歸)
    linearRegression(data, daysToPredict = 30) {
        const n = data.length;
        let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
        
        data.forEach((item, index) => {
            sumX += index;
            sumY += item.tvl;
            sumXY += index * item.tvl;
            sumX2 += index * index;
        });

        const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
        const intercept = (sumY - slope * sumX) / n;

        const predictions = [];
        for (let i = 0; i < daysToPredict; i++) {
            const predictedTVL = intercept + slope * (n + i);
            predictions.push({
                day: i + 1,
                predictedTVL: Math.max(0, predictedTVL),
                date: new Date(Date.now() + (i + 1) * 86400000)
                    .toISOString().split('T')[0]
            });
        }

        return {
            slope: slope,
            rSquared: this.calculateRSquared(data, slope, intercept),
            predictions: predictions
        };
    }

    calculateRSquared(data, slope, intercept) {
        const n = data.length;
        const mean = data.reduce((a, b) => a + b.tvl, 0) / n;
        
        let ssTot = 0, ssRes = 0;
        data.forEach((item, index) => {
            const predicted = intercept + slope * index;
            ssTot += Math.pow(item.tvl - mean, 2);
            ssRes += Math.pow(item.tvl - predicted, 2);
        });

        return 1 - (ssRes / ssTot);
    }
}

module.exports = TVLAdvancedAnalyzer;

第三章:Gas 費用歷史走勢圖

3.1 Gas 費用數據收集與處理

Gas 費用是以太坊網路擁堵程度的直接指標。完整的 Gas 費用監控系統需要收集多個維度的數據:

// ethereum-dashboard/data-sources/gas-tracker.js
const axios = require('axios');

class GasFeeTracker {
    constructor() {
        this.historicalData = [];
        this.maxHistory = 365 * 24; // 保留一年的小時數據
    }

    // 從多個來源獲取 Gas 費用數據
    async fetchCurrentGasPrices() {
        const sources = [
            this.fetchEtherscanGas(),
            this.fetchEthGasStation(),
            this.fetchBlocknative()
        ];

        try {
            const results = await Promise.allSettled(sources);
            const validResults = results
                .filter(r => r.status === 'fulfilled' && r.value)
                .map(r => r.value);

            if (validResults.length === 0) {
                return null;
            }

            // 取平均值
            return {
                slow: this.avg(validResults.map(r => r.slow)),
                standard: this.avg(validResults.map(r => r.standard)),
                fast: this.avg(validResults.map(r => r.fast)),
                baseFee: validResults[0].baseFee,
                timestamp: Date.now()
            };
        } catch (error) {
            console.error('Gas fetch error:', error);
            return null;
        }
    }

    async fetchEtherscanGas() {
        try {
            const response = await axios.get(
                'https://api.etherscan.io/api?module=gastracker&action=gasoracle'
            );
            
            if (response.data.status === '1') {
                const result = response.data.result;
                return {
                    slow: parseInt(result.SafeGasPrice),
                    standard: parseInt(result.ProposeGasPrice),
                    fast: parseInt(result.FastGasPrice),
                    baseFee: parseFloat(result.suggestBaseFee)
                };
            }
        } catch (error) {
            console.error('Etherscan gas error:', error);
        }
        return null;
    }

    async fetchEthGasStation() {
        try {
            const response = await axios.get(
                'https://ethgasstation.info/api/ethgasAPI.json'
            );
            
            return {
                slow: response.data.safeLow / 10,
                standard: response.data.average / 10,
                fast: response.data.fast / 10,
                baseFee: response.data.baseFee / 10
            };
        } catch (error) {
            return null;
        }
    }

    async fetchBlocknative() {
        try {
            // Blocknative 需要 API Key,這裡是示例
            const response = await axios.get(
                'https://api.blocknative.com/gasprices'
            );
            
            const blockPrices = response.data.result.blockPrices[0];
            const estimatedPrices = blockPrices.estimatedPrices[0];
            
            return {
                slow: estimatedPrices.confidenceLevels[0].price,
                standard: estimatedPrices.confidenceLevels[1].price,
                fast: estimatedPrices.confidenceLevels[2].price,
                baseFee: estimatedPrices.baseFee
            };
        } catch (error) {
            return null;
        }
    }

    avg(numbers) {
        return numbers.reduce((a, b) => a + b, 0) / numbers.length;
    }

    // 記錄歷史數據
    async recordGasData() {
        const currentGas = await this.fetchCurrentGasPrices();
        
        if (currentGas) {
            const record = {
                timestamp: currentGas.timestamp,
                date: new Date(currentGas.timestamp).toISOString(),
                slow: currentGas.slow,
                standard: currentGas.standard,
                fast: currentGas.fast,
                baseFee: currentGas.baseFee,
                // 計算 USD 成本(假設 ETH 價格)
                slowUSD: currentGas.slow * 21000 * 3000 / 1e9,
                standardUSD: currentGas.standard * 21000 * 3000 / 1e9,
                fastUSD: currentGas.fast * 21000 * 3000 / 1e9
            };

            this.historicalData.push(record);
            
            // 保持歷史數據在限制內
            if (this.historicalData.length > this.maxHistory) {
                this.historicalData.shift();
            }

            return record;
        }
        
        return null;
    }

    // 獲取歷史數據(按小時/天匯總)
    getHistoricalData(period = 30, aggregation = 'hour') {
        const now = Date.now();
        const periodMs = period * 24 * 60 * 60 * 1000;
        const cutoff = now - periodMs;

        const filtered = this.historicalData.filter(r => r.timestamp > cutoff);

        if (aggregation === 'day') {
            // 按天匯總
            const dailyData = {};
            
            filtered.forEach(record => {
                const date = record.date.split('T')[0];
                
                if (!dailyData[date]) {
                    dailyData[date] = {
                        slow: [],
                        standard: [],
                        fast: [],
                        baseFee: []
                    };
                }
                
                dailyData[date].slow.push(record.slow);
                dailyData[date].standard.push(record.standard);
                dailyData[date].fast.push(record.fast);
                dailyData[date].baseFee.push(record.baseFee);
            });

            return Object.entries(dailyData).map(([date, values]) => ({
                date: date,
                slow: this.avg(values.slow),
                standard: this.avg(values.standard),
                fast: this.avg(values.fast),
                baseFee: this.avg(values.baseFee)
            }));
        }

        return filtered;
    }
}

module.exports = GasFeeTracker;

3.2 Gas 費用走勢圖視覺化

以下是一個專業級的 Gas 費用走勢圖範例:

// ethereum-dashboard/charts/gas-chart.js
class GasChartRenderer {
    constructor(canvasId) {
        this.canvas = document.getElementById(canvasId);
        this.ctx = this.canvas.getContext('2d');
        this.chart = null;
    }

    render(data, options = {}) {
        const {
            showBaseFee = true,
            showMA = true,
            maPeriod = 24,
            colorScheme = 'dark'
        } = options;

        const isDark = colorScheme === 'dark';
        const colors = {
            slow: isDark ? '#3fb950' : '#16a34a',
            standard: isDark ? '#58a6ff' : '#2563eb',
            fast: isDark ? '#f85149' : '#dc2626',
            baseFee: isDark ? '#a371f7' : '#9333ea',
            ma: isDark ? '#f0883e' : '#ea580c',
            grid: isDark ? '#30363d' : '#e5e7eb',
            text: isDark ? '#8b949e' : '#6b7280'
        };

        // 準備數據
        const labels = data.map(d => {
            const date = new Date(d.date);
            return date.toLocaleDateString('zh-TW', { 
                month: 'short', 
                day: 'numeric',
                hour: '2-digit'
            });
        });

        const slowData = data.map(d => d.slow);
        const standardData = data.map(d => d.standard);
        const fastData = data.map(d => d.fast);
        const baseFeeData = data.map(d => d.baseFee);

        // 計算移動平均
        let maData = null;
        if (showMA && data.length > maPeriod) {
            maData = this.calculateMA(standardData, maPeriod);
        }

        const datasets = [
            {
                label: 'Slow (安全)',
                data: slowData,
                borderColor: colors.slow,
                backgroundColor: 'transparent',
                borderWidth: 2,
                tension: 0.3,
                pointRadius: 0
            },
            {
                label: 'Standard (標準)',
                data: standardData,
                borderColor: colors.standard,
                backgroundColor: 'transparent',
                borderWidth: 2,
                tension: 0.3,
                pointRadius: 0
            },
            {
                label: 'Fast (快速)',
                data: fastData,
                borderColor: colors.fast,
                backgroundColor: 'transparent',
                borderWidth: 2,
                tension: 0.3,
                pointRadius: 0
            }
        ];

        if (showBaseFee) {
            datasets.push({
                label: 'Base Fee',
                data: baseFeeData,
                borderColor: colors.baseFee,
                borderDash: [5, 5],
                backgroundColor: 'transparent',
                borderWidth: 1,
                tension: 0.3,
                pointRadius: 0
            });
        }

        if (maData) {
            datasets.push({
                label: `${maPeriod}小時均線`,
                data: maData,
                borderColor: colors.ma,
                backgroundColor: 'transparent',
                borderWidth: 3,
                tension: 0.3,
                pointRadius: 0
            });
        }

        if (this.chart) {
            this.chart.destroy();
        }

        this.chart = new Chart(this.ctx, {
            type: 'line',
            data: { labels, datasets },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                interaction: {
                    intersect: false,
                    mode: 'index'
                },
                plugins: {
                    legend: {
                        position: 'top',
                        labels: {
                            color: colors.text,
                            usePointStyle: true,
                            padding: 20
                        }
                    },
                    tooltip: {
                        backgroundColor: isDark ? '#161b22' : '#ffffff',
                        titleColor: isDark ? '#c9d1d9' : '#1f2937',
                        bodyColor: isDark ? '#c9d1d9' : '#1f2937',
                        borderColor: colors.grid,
                        borderWidth: 1,
                        callbacks: {
                            label: function(context) {
                                return `${context.dataset.label}: ${context.raw.toFixed(2)} Gwei`;
                            }
                        }
                    }
                },
                scales: {
                    x: {
                        grid: { color: colors.grid },
                        ticks: {
                            color: colors.text,
                            maxTicksLimit: 12,
                            maxRotation: 0
                        }
                    },
                    y: {
                        grid: { color: colors.grid },
                        ticks: {
                            color: colors.text,
                            callback: function(value) {
                                return value + ' Gwei';
                            }
                        },
                        title: {
                            display: true,
                            text: 'Gas Price (Gwei)',
                            color: colors.text
                        }
                    }
                }
            }
        });
    }

    calculateMA(data, period) {
        const result = [];
        for (let i = 0; i < data.length; i++) {
            if (i < period - 1) {
                result.push(null);
            } else {
                const slice = data.slice(i - period + 1, i + 1);
                result.push(slice.reduce((a, b) => a + b, 0) / period);
            }
        }
        return result;
    }
}

// 使用範例
const gasTracker = new GasFeeTracker();
const gasChart = new GasChartRenderer('gasChart');

// 即時更新
async function updateGasChart() {
    await gasTracker.recordGasData();
    const historicalData = gasTracker.getHistoricalData(7, 'hour');
    gasChart.render(historicalData);
}

// 每分鐘更新
setInterval(updateGasChart, 60000);
updateGasChart();

3.3 Gas 費用預測模型

基於歷史數據,我們可以建立簡單的 Gas 費用預測模型:

// ethereum-dashboard/analytics/gas-forecast.js
class GasPriceForecaster {
    constructor(historicalData) {
        this.data = historicalData;
    }

    // 基於簡單移動平均的預測
    simpleForecast(hours = 24) {
        const recent = this.data.slice(-24);
        const avgStandard = recent.reduce((a, b) => a + b.standard, 0) / recent.length;
        const avgFast = recent.reduce((a, b) => a + b.fast, 0) / recent.length;
        const avgSlow = recent.reduce((a, b) => a + b.slow, 0) / recent.length;

        return {
            standard: avgStandard,
            fast: avgFast,
            slow: avgSlow,
            confidence: this.calculateConfidence()
        };
    }

    // 基於趨勢的預測
    trendForecast(hours = 24) {
        if (this.data.length < 48) {
            return this.simpleForecast(hours);
        }

        const recent = this.data.slice(-48);
        
        // 計算斜率
        const xValues = recent.map((_, i) => i);
        const yValues = recent.map(d => d.standard);
        
        const n = xValues.length;
        const sumX = xValues.reduce((a, b) => a + b, 0);
        const sumY = yValues.reduce((a, b) => a + b, 0);
        const sumXY = xValues.reduce((a, x, i) => a + x * yValues[i], 0);
        const sumX2 = xValues.reduce((a, b) => a + b * b, 0);
        
        const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
        const intercept = (sumY - slope * sumX) / n;

        // 預測未來
        const predictions = [];
        for (let i = 0; i < hours; i++) {
            const predictedValue = intercept + slope * (n + i);
            predictions.push(Math.max(1, predictedValue)); // 最低 1 Gwei
        }

        return {
            predictions: predictions,
            currentSlope: slope,
            baseValue: intercept,
            trend: slope > 0.5 ? 'rising' : slope < -0.5 ? 'falling' : 'stable'
        };
    }

    // 計算預測置信度
    calculateConfidence() {
        if (this.data.length < 24) return 'low';
        
        const recent = this.data.slice(-24);
        const variance = this.calculateVariance(recent.map(d => d.standard));
        const mean = recent.reduce((a, b) => a + b.standard, 0) / recent.length;
        const cv = Math.sqrt(variance) / mean; // 變異係數
        
        if (cv < 0.1) return 'high';
        if (cv < 0.3) return 'medium';
        return 'low';
    }

    calculateVariance(values) {
        const mean = values.reduce((a, b) => a + b, 0) / values.length;
        return values.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / values.length;
    }

    // 獲取建議的 Gas 價格
    getSuggestedGasPrices() {
        const forecast = this.trendForecast();
        
        // 根據預測趨勢調整價格建議
        let multiplier = 1;
        if (forecast.trend === 'rising') {
            multiplier = 1.2;
        } else if (forecast.trend === 'falling') {
            multiplier = 0.9;
        }

        return {
            slow: Math.round(forecast.baseValue * 0.8 * multiplier),
            standard: Math.round(forecast.baseValue * multiplier),
            fast: Math.round(forecast.baseValue * 1.3 * multiplier),
            trend: forecast.trend,
            confidence: this.calculateConfidence(),
            updatedAt: new Date().toISOString()
        };
    }
}

module.exports = GasPriceForecaster;

第四章:整合儀表板建構

4.1 完整儀表板架構

以下是一個完整的以太坊生態儀表板的整合範例:

// ethereum-dashboard/dashboard.js
const DeFiLlamaAPI = require('./data-sources/defi-llama');
const GasFeeTracker = require('./data-sources/gas-tracker');
const TVLTrendProcessor = require('./charts/tvl-trend');
const TVLAdvancedAnalyzer = require('./charts/tvl-advanced');

class EthereumDashboard {
    constructor(config = {}) {
        this.config = {
            refreshInterval: config.refreshInterval || 60000,
            tvlDays: config.tvlDays || 30,
            gasHistoryHours: config.gasHistoryHours || 168,
            ...config
        };

        this.llama = new DeFiLlamaAPI();
        this.gasTracker = new GasFeeTracker();
        this.tvlProcessor = new TVLTrendProcessor();
        this.tvlAnalyzer = new TVLAdvancedAnalyzer();

        this.data = {
            tvl: null,
            tvlHistory: null,
            gas: null,
            gasHistory: null,
            layer2: null,
            validators: null,
            lastUpdate: null
        };

        this.subscribers = [];
    }

    // 初始化儀表板
    async initialize() {
        console.log('Initializing Ethereum Dashboard...');
        
        // 載入歷史數據
        await this.refreshAllData();
        
        // 設定定期刷新
        this.startAutoRefresh();
        
        console.log('Dashboard initialized');
        return this;
    }

    // 刷新所有數據
    async refreshAllData() {
        try {
            const [
                tvlData,
                tvlHistory,
                gasData,
                gasHistory,
                layer2Data
            ] = await Promise.all([
                this.fetchTVLData(),
                this.fetchTVLHistory(),
                this.fetchGasData(),
                this.fetchGasHistory(),
                this.fetchLayer2Data()
            ]);

            this.data = {
                tvl: tvlData,
                tvlHistory: tvlHistory,
                gas: gasData,
                gasHistory: gasHistory,
                layer2: layer2Data,
                lastUpdate: new Date()
            };

            // 通知訂閱者
            this.notifySubscribers();

            return this.data;
        } catch (error) {
            console.error('Error refreshing dashboard data:', error);
            throw error;
        }
    }

    async fetchTVLData() {
        const topProtocols = await this.llama.getEthereumTopProtocols(10);
        const multiChain = await this.llama.getMultiChainTVL();
        
        const ethereumData = multiChain.find(c => c.name === 'Ethereum');
        
        return {
            total: ethereumData?.tvl || 0,
            change24h: ethereumData?.tvlChange24h || 0,
            change7d: ethereumData?.tvlChange7d || 0,
            topProtocols: topProtocols,
            multiChain: multiChain.slice(0, 10)
        };
    }

    async fetchTVLHistory() {
        return await this.tvlProcessor.processTVLTrend('Ethereum', this.config.tvlDays);
    }

    async fetchGasData() {
        return await this.gasTracker.fetchCurrentGasPrices();
    }

    async fetchGasHistory() {
        // 先記錄當前數據
        await this.gasTracker.recordGasData();
        
        // 獲取歷史數據(小時級別)
        const hours = Math.ceil(this.config.gasHistoryHours / 24) * 24;
        return this.gasTracker.getHistoricalData(hours / 24, 'hour');
    }

    async fetchLayer2Data() {
        try {
            const response = await axios.get(
                'https://api.llama.fi/overview/chainvol?chains=Arbitrum,Optimism,Base,zkSync,Starknet,Polygon,Linea,Scroll'
            );
            
            return response.data.map(chain => ({
                name: chain.name,
                tvl: chain.tvl,
                change24h: chain.change1d,
                change7d: chain.change7d,
                category: 'Layer2'
            }));
        } catch (error) {
            console.error('Layer2 data fetch error:', error);
            return [];
        }
    }

    // 訂閱數據更新
    subscribe(callback) {
        this.subscribers.push(callback);
        return () => {
            this.subscribers = this.subscribers.filter(cb => cb !== callback);
        };
    }

    notifySubscribers() {
        this.subscribers.forEach(cb => cb(this.data));
    }

    // 自動刷新
    startAutoRefresh() {
        this.intervalId = setInterval(
            () => this.refreshAllData(),
            this.config.refreshInterval
        );
    }

    stopAutoRefresh() {
        if (this.intervalId) {
            clearInterval(this.intervalId);
            this.intervalId = null;
        }
    }

    // 獲取儀表板快照(用於導出)
    getSnapshot() {
        return {
            generatedAt: new Date().toISOString(),
            data: this.data,
            summary: this.generateSummary()
        };
    }

    generateSummary() {
        if (!this.data.tvl || !this.data.gas) return null;

        return {
            tvlFormatted: this.formatTVL(this.data.tvl.total),
            tvlChangeFormatted: `${this.data.tvl.change24h > 0 ? '+' : ''}${this.data.tvl.change24h.toFixed(2)}%`,
            gasStandard: `${this.data.gas.standard} Gwei`,
            gasFast: `${this.data.gas.fast} Gwei`,
            gasSlow: `${this.data.gas.slow} Gwei`,
            layer2Count: this.data.layer2?.length || 0,
            layer2TVLFormatted: this.formatTVL(
                this.data.layer2?.reduce((a, b) => a + b.tvl, 0) || 0
            )
        };
    }

    formatTVL(value) {
        if (value >= 1e12) return `$${(value / 1e12).toFixed(2)}T`;
        if (value >= 1e9) return `$${(value / 1e9).toFixed(2)}B`;
        if (value >= 1e6) return `$${(value / 1e6).toFixed(2)}M`;
        return `$${value.toFixed(0)}`;
    }
}

module.exports = EthereumDashboard;

4.2 即時數據監控面板

<!-- ethereum-realtime-dashboard.html -->
<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>以太坊生態即時監控儀表板</title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background: #0d1117;
            color: #c9d1d9;
            padding: 20px;
        }
        .dashboard {
            max-width: 1600px;
            margin: 0 auto;
        }
        .header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 24px;
            padding-bottom: 16px;
            border-bottom: 1px solid #30363d;
        }
        .header h1 {
            font-size: 24px;
            color: #58a6ff;
        }
        .last-update {
            font-size: 12px;
            color: #8b949e;
        }
        .grid {
            display: grid;
            grid-template-columns: repeat(4, 1fr);
            gap: 16px;
            margin-bottom: 24px;
        }
        .card {
            background: #161b22;
            border-radius: 8px;
            padding: 20px;
            border: 1px solid #30363d;
        }
        .card-title {
            font-size: 12px;
            color: #8b949e;
            text-transform: uppercase;
            letter-spacing: 0.5px;
            margin-bottom: 8px;
        }
        .card-value {
            font-size: 28px;
            font-weight: bold;
        }
        .card-change {
            font-size: 14px;
            margin-top: 4px;
        }
        .positive { color: #3fb950; }
        .negative { color: #f85149; }
        
        .chart-row {
            display: grid;
            grid-template-columns: 2fr 1fr;
            gap: 16px;
            margin-bottom: 24px;
        }
        .chart-container {
            background: #161b22;
            border-radius: 8px;
            padding: 20px;
            height: 400px;
        }
        
        .gas-indicators {
            display: flex;
            flex-direction: column;
            gap: 12px;
        }
        .gas-level {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 16px;
            background: #0d1117;
            border-radius: 6px;
        }
        .gas-label {
            font-size: 14px;
            color: #8b949e;
        }
        .gas-price {
            font-size: 20px;
            font-weight: bold;
        }
        .gas-usd {
            font-size: 12px;
            color: #8b949e;
        }
        
        .protocol-table {
            width: 100%;
            border-collapse: collapse;
        }
        .protocol-table th,
        .protocol-table td {
            padding: 12px;
            text-align: left;
            border-bottom: 1px solid #30363d;
        }
        .protocol-table th {
            font-size: 12px;
            color: #8b949e;
            text-transform: uppercase;
        }
        .loading {
            text-align: center;
            padding: 40px;
            color: #8b949e;
        }
    </style>
</head>
<body>
    <div class="dashboard">
        <div class="header">
            <h1>以太坊生態監控</h1>
            <div class="last-update">最後更新: <span id="lastUpdate">載入中...</span></div>
        </div>

        <!-- 關鍵指標卡片 -->
        <div class="grid">
            <div class="card">
                <div class="card-title">以太坊 TVL</div>
                <div class="card-value" id="tvlValue">--</div>
                <div class="card-change" id="tvlChange">--</div>
            </div>
            <div class="card">
                <div class="card-title">Layer 2 TVL</div>
                <div class="card-value" id="l2TVL">--</div>
                <div class="card-change" id="l2Change">--</div>
            </div>
            <div class="card">
                <div class="card-title">Gas 費用 (Standard)</div>
                <div class="card-value" id="gasValue">--</div>
                <div class="card-change" id="gasUSD">--</div>
            </div>
            <div class="card">
                <div class="card-title">網路狀態</div>
                <div class="card-value" id="networkStatus">正常</div>
                <div class="card-change" id="blockTime">--</div>
            </div>
        </div>

        <!-- 圖表區域 -->
        <div class="chart-row">
            <div class="chart-container">
                <canvas id="tvlChart"></canvas>
            </div>
            <div class="chart-container">
                <div class="card-title" style="margin-bottom: 16px;">即時 Gas 費用</div>
                <div class="gas-indicators">
                    <div class="gas-level">
                        <div>
                            <div class="gas-label">慢 (Safe)</div>
                            <div class="gas-usd" id="gasSlowUSD">~$0</div>
                        </div>
                        <div class="gas-price" id="gasSlow" style="color: #3fb950;">--</div>
                    </div>
                    <div class="gas-level">
                        <div>
                            <div class="gas-label">標準 (Propose)</div>
                            <div class="gas-usd" id="gasStandardUSD">~$0</div>
                        </div>
                        <div class="gas-price" id="gasStandard" style="color: #58a6ff;">--</div>
                    </div>
                    <div class="gas-level">
                        <div>
                            <div class="gas-label">快 (Fast)</div>
                            <div class="gas-usd" id="gasFastUSD">~$0</div>
                        </div>
                        <div class="gas-price" id="gasFast" style="color: #f85149;">--</div>
                    </div>
                </div>
            </div>
        </div>

        <!-- Layer 2 數據 -->
        <div class="card">
            <div class="card-title" style="margin-bottom: 16px;">Layer 2 TVL 排名</div>
            <table class="protocol-table">
                <thead>
                    <tr>
                        <th>排名</th>
                        <th>名稱</th>
                        <th>TVL</th>
                        <th>24h 變化</th>
                        <th>7d 變化</th>
                    </tr>
                </thead>
                <tbody id="l2Table">
                    <tr><td colspan="5" class="loading">載入中...</td></tr>
                </tbody>
            </table>
        </div>
    </div>

    <script>
        // 格式化函數
        function formatTVL(value) {
            if (!value) return '--';
            if (value >= 1e12) return '$' + (value / 1e12).toFixed(2) + 'T';
            if (value >= 1e9) return '$' + (value / 1e9).toFixed(2) + 'B';
            if (value >= 1e6) return '$' + (value / 1e6).toFixed(2) + 'M';
            return '$' + (value / 1e3).toFixed(2) + 'K';
        }

        function formatChange(value) {
            if (!value) return '--';
            const sign = value >= 0 ? '+' : '';
            return sign + value.toFixed(2) + '%';
        }

        function formatGweiUSD(gwei) {
            // 假設 ETH 價格為 $3000,基本轉帳需要 21000 gas
            const eth = gwei * 21000 / 1e9;
            return '$' + eth.toFixed(2);
        }

        // API 獲取數據
        async function fetchDashboardData() {
            try {
                // 獲取 TVL 數據
                const tvlRes = await fetch('https://api.llama.fi/chains');
                const tvlData = await tvlRes.json();
                const ethereum = tvlData.find(c => c.name === 'Ethereum');
                const layer2s = tvlData.filter(c => 
                    ['Arbitrum', 'Optimism', 'Base', 'zkSync Era', 'Starknet', 'Polygon zkEVM', 'Linea', 'Scroll'].includes(c.name)
                );

                // 獲取 Gas 數據
                const gasRes = await fetch(
                    'https://api.etherscan.io/api?module=gastracker&action=gasoracle'
                );
                const gasData = await gasRes.json();

                return {
                    tvl: ethereum,
                    layer2s: layer2s,
                    gas: gasData.result
                };
            } catch (error) {
                console.error('Fetch error:', error);
                return null;
            }
        }

        // 更新 UI
        function updateDashboard(data) {
            if (!data) return;

            // 更新時間
            document.getElementById('lastUpdate').textContent = 
                new Date().toLocaleString('zh-TW');

            // 更新 TVL
            if (data.tvl) {
                document.getElementById('tvlValue').textContent = 
                    formatTVL(data.tvl.tvl);
                const tvlChange = document.getElementById('tvlChange');
                tvlChange.textContent = formatChange(data.tvl.change7d);
                tvlChange.className = 'card-change ' + 
                    (data.tvl.change7d >= 0 ? 'positive' : 'negative');
            }

            // 更新 Layer 2
            if (data.layer2s) {
                const totalL2TVL = data.layer2s.reduce((sum, l2) => sum + l2.tvl, 0);
                const avgChange = data.layer2s.reduce((sum, l2) => sum + l2.change7d, 0) / data.layer2s.length;
                
                document.getElementById('l2TVL').textContent = formatTVL(totalL2TVL);
                const l2Change = document.getElementById('l2Change');
                l2Change.textContent = formatChange(avgChange);
                l2Change.className = 'card-change ' + (avgChange >= 0 ? 'positive' : 'negative');

                // 更新表格
                const tbody = document.getElementById('l2Table');
                tbody.innerHTML = data.layer2s
                    .sort((a, b) => b.tvl - a.tvl)
                    .slice(0, 10)
                    .map((l2, i) => `
                        <tr>
                            <td>${i + 1}</td>
                            <td>${l2.name}</td>
                            <td>${formatTVL(l2.tvl)}</td>
                            <td class="${l2.change1d >= 0 ? 'positive' : 'negative'}">
                                ${formatChange(l2.change1d)}
                            </td>
                            <td class="${l2.change7d >= 0 ? 'positive' : 'negative'}">
                                ${formatChange(l2.change7d)}
                            </td>
                        </tr>
                    `).join('');
            }

            // 更新 Gas
            if (data.gas) {
                const slow = data.gas.SafeGasPrice;
                const standard = data.gas.ProposeGasPrice;
                const fast = data.gas.FastGasPrice;

                document.getElementById('gasSlow').textContent = slow + ' Gwei';
                document.getElementById('gasStandard').textContent = standard + ' Gwei';
                document.getElementById('gasFast').textContent = fast + ' Gwei';
                
                document.getElementById('gasValue').textContent = standard + ' Gwei';
                document.getElementById('gasUSD').textContent = 
                    formatGweiUSD(standard);

                document.getElementById('gasSlowUSD').textContent = 
                    formatGweiUSD(slow);
                document.getElementById('gasStandardUSD').textContent = 
                    formatGweiUSD(standard);
                document.getElementById('gasFastUSD').textContent = 
                    formatGweiUSD(fast);
            }
        }

        // 初始化
        async function init() {
            const data = await fetchDashboardData();
            updateDashboard(data);
            
            // 每分鐘刷新
            setInterval(async () => {
                const data = await fetchDashboardData();
                updateDashboard(data);
            }, 60000);
        }

        init();
    </script>
</body>
</html>

第五章:自動化與警報系統

5.1 數據異常檢測

// ethereum-dashboard/alerts/anomaly-detector.js
class AnomalyDetector {
    constructor(config = {}) {
        this.thresholds = {
            tvl: {
                dropPercent: config.tvlDropThreshold || 10,
                surgePercent: config.tvlSurgeThreshold || 20
            },
            gas: {
                highPercent: config.gasHighThreshold || 100,
                lowPercent: config.gasLowThreshold || -50
            }
        };
        
        this.history = [];
        this.alerts = [];
    }

    // 檢測 TVL 異常
    detectTVLAnomaly(currentTVL, previousTVL) {
        if (!previousTVL || previousTVL === 0) return null;
        
        const changePercent = ((currentTVL - previousTVL) / previousTVL) * 100;
        
        if (Math.abs(changePercent) < this.thresholds.tvl.dropPercent) {
            return null;
        }

        const anomaly = {
            type: changePercent > 0 ? 'surge' : 'drop',
            metric: 'TVL',
            currentValue: currentTVL,
            previousValue: previousTVL,
            changePercent: changePercent,
            timestamp: new Date(),
            severity: Math.abs(changePercent) > 20 ? 'high' : 'medium'
        };

        this.alerts.push(anomaly);
        return anomaly;
    }

    // 檢測 Gas 費用異常
    detectGasAnomaly(currentGas, historicalAvg) {
        if (!historicalAvg || historicalAvg === 0) return null;
        
        const changePercent = ((currentGas - historicalAvg) / historicalAvg) * 100;
        
        if (Math.abs(changePercent) < this.thresholds.gas.highPercent) {
            return null;
        }

        const anomaly = {
            type: changePercent > 0 ? 'spike' : 'drop',
            metric: 'Gas',
            currentValue: currentGas,
            averageValue: historicalAvg,
            changePercent: changePercent,
            timestamp: new Date(),
            severity: currentGas > 100 ? 'high' : 'medium',
            recommendation: this.getGasRecommendation(currentGas, changePercent)
        };

        this.alerts.push(anomaly);
        return anomaly;
    }

    getGasRecommendation(gas, changePercent) {
        if (changePercent > 100) {
            return 'Gas 費用異常飆升,建議推遲非緊急性交易或使用 Layer 2';
        } else if (changePercent > 50) {
            return 'Gas 費用偏高,可考慮設定較高 Gas 優先級';
        } else if (changePercent < -30) {
            return 'Gas 費用處於低位,適合進行大額交易';
        }
        return 'Gas 費用正常';
    }

    // 獲取最近警報
    getRecentAlerts(hours = 24) {
        const cutoff = Date.now() - hours * 60 * 60 * 1000;
        return this.alerts.filter(a => a.timestamp.getTime() > cutoff);
    }

    // 清空警報
    clearAlerts() {
        this.alerts = [];
    }
}

module.exports = AnomalyDetector;

5.2 自動化報告生成

// ethereum-dashboard/reports/auto-reporter.js
class EthereumReportGenerator {
    constructor(dashboard) {
        this.dashboard = dashboard;
    }

    // 生成每日報告
    generateDailyReport() {
        const data = this.dashboard.getSnapshot();
        const summary = data.summary;
        
        return {
            title: `以太坊生態每日報告 - ${data.generatedAt.split('T')[0]}`,
            summary: {
                tvl: summary.tvlFormatted,
                tvlChange: summary.tvlChangeFormatted,
                gasStandard: summary.gasStandard,
                layer2TVL: summary.layer2TVLFormatted
            },
            recommendations: this.generateRecommendations(data),
            generatedAt: data.generatedAt
        };
    }

    generateRecommendations(data) {
        const recommendations = [];
        const gas = data.data.gas;
        const tvl = data.data.tvl;

        // Gas 建議
        if (gas && gas.standard > 50) {
            recommendations.push({
                type: 'gas',
                priority: 'high',
                message: 'Gas 費用偏高 (>50 Gwei),建議推遲大額交易或考慮使用 Layer 2'
            });
        }

        // TVL 建議
        if (tvl && tvl.change24h < -5) {
            recommendations.push({
                type: 'tvl',
                priority: 'medium',
                message: 'TVL 日變化下降超過 5%,建議關注 DeFi 市場風險'
            });
        }

        return recommendations;
    }

    // 導出為 Markdown 格式
    exportAsMarkdown() {
        const report = this.generateDailyReport();
        
        return `# ${report.title}

## 摘要

- **以太坊 TVL**: ${report.summary.tvl} (${report.summary.tvlChange})
- **Gas 費用 (標準)**: ${report.summary.gasStandard}
- **Layer 2 TVL**: ${report.summary.layer2TVL}

## 建議

${report.recommendations.map(r => `- [${r.priority.toUpperCase()}] ${r.message}`).join('\n')}

---
*報告生成時間: ${report.generatedAt}*
`;
    }
}

module.exports = EthereumReportGenerator;

結論

本指南詳細介紹了如何建構一個專業的以太坊生態數據儀表板。從數據來源的選擇與 API 整合、TVL 趨勢圖的繪製與分析、Gas 費用走勢圖的即時監控,到完整的儀表板架構與自動化警報系統,我們提供了完整的技術解決方案和可直接部署的程式碼範例。

關鍵要點總結:

首先,在數據來源方面,推薦使用 DeFi Llama 獲取 TVL 數據、Etherscan API 獲取 Gas 費用數據、以及 The Graph 查詢深度的 DeFi 協議數據。這些服務都有免費層可以使用,足以支撐中小型項目。

其次,在數據視覺化方面,Chart.js 是一個功能強大且易於使用的圖表庫,支援多種圖表類型和豐富的自訂選項。對於更複雜的需求,可以考慮使用 D3.js 或專業的 BI 工具如 Metabase。

第三,在即時監控方面,設定適當的刷新頻率很重要。對於 Gas 費用等高頻變化的數據,建議每分鐘刷新一次;對於 TVL 等相對穩定的數據,每 5-15 分鐘刷新一次即可。

最後,在警報系統方面,建立異常檢測機制可以幫助及時發現問題。可以根據實際需求設定閾值,並通過 Email、Discord、Webhook 等方式發送通知。

有了這些工具和技術,開發者可以根據自己的需求構建客製化的以太坊生態監控儀表板,及時掌握以太坊生態的發展動態。


相關資源

聲明:本指南僅供技術參考,不構成投資建議。加密貨幣投資具有高度風險,請在做出任何投資決策前進行獨立研究。

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。

目前尚無評論,成為第一個發表評論的人吧!