Разработка тепловой карты крипторынка
Тепловая карта (heatmap) криптовалютного рынка — это визуализация изменения цен всего рынка одновременно. Каждый актив представлен прямоугольником, размер которого пропорционален капитализации, цвет отражает процентное изменение за период. Незаменимый инструмент для быстрого понимания рыночной структуры.
Алгоритм Treemap Layout
Основа heatmap — алгоритм Treemap, оптимально упаковывающий прямоугольники разного размера:
interface HeatmapCell {
symbol: string;
marketCap: number;
changePercent: number;
price: number;
volume24h: number;
x: number; // вычисляется алгоритмом
y: number;
width: number;
height: number;
}
class SquarifiedTreemap {
layout(
data: HeatmapCell[],
bounds: {x: number; y: number; width: number; height: number}
): HeatmapCell[] {
// Сортируем по убыванию размера (capitalisation)
const sorted = [...data].sort((a, b) => b.marketCap - a.marketCap);
const totalMarketCap = sorted.reduce((sum, d) => sum + d.marketCap, 0);
return this.squarify(sorted, bounds, totalMarketCap);
}
private squarify(items: HeatmapCell[], bounds: Bounds, total: number): HeatmapCell[] {
if (items.length === 0) return [];
const isHorizontal = bounds.width >= bounds.height;
const result: HeatmapCell[] = [];
let row: HeatmapCell[] = [];
let rowArea = 0;
let offset = 0;
for (const item of items) {
const itemArea = (item.marketCap / total) * (bounds.width * bounds.height);
const testRow = [...row, item];
const testArea = rowArea + itemArea;
if (this.wouldImproveAspectRatio(testRow, testArea, bounds, isHorizontal)) {
row.push(item);
rowArea = testArea;
} else {
// Финализируем текущую строку
result.push(...this.layoutRow(row, rowArea, bounds, isHorizontal, offset, total));
const rowThickness = rowArea / (isHorizontal ? bounds.width : bounds.height);
offset += rowThickness;
row = [item];
rowArea = itemArea;
}
}
if (row.length > 0) {
result.push(...this.layoutRow(row, rowArea, bounds, isHorizontal, offset, total));
}
return result;
}
}
Получение данных
import httpx
import asyncio
class MarketDataProvider:
COINGECKO_URL = "https://api.coingecko.com/api/v3"
async def get_heatmap_data(
self,
vs_currency: str = 'usd',
top_n: int = 100
) -> list[dict]:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.COINGECKO_URL}/coins/markets",
params={
"vs_currency": vs_currency,
"order": "market_cap_desc",
"per_page": top_n,
"page": 1,
"sparkline": False,
"price_change_percentage": "1h,24h,7d"
}
)
coins = response.json()
return [
{
"symbol": c["symbol"].upper(),
"name": c["name"],
"market_cap": c["market_cap"] or 0,
"change_1h": c.get("price_change_percentage_1h_in_currency", 0) or 0,
"change_24h": c.get("price_change_percentage_24h", 0) or 0,
"change_7d": c.get("price_change_percentage_7d_in_currency", 0) or 0,
"volume_24h": c.get("total_volume", 0) or 0,
"price": c["current_price"],
"image": c["image"]
}
for c in coins if c["market_cap"]
]
Кэшируем данные: CoinGecko бесплатный API — 10-30 запросов/мин. Обновляем каждые 60 секунд:
async def get_cached_data(self) -> list[dict]:
cache_key = "heatmap_data"
cached = await self.redis.get(cache_key)
if cached:
return json.loads(cached)
data = await self.get_heatmap_data()
await self.redis.setex(cache_key, 60, json.dumps(data))
return data
React визуализация
import React, { useMemo } from 'react';
const getColor = (changePercent: number): string => {
const intensity = Math.min(Math.abs(changePercent) / 10, 1);
if (changePercent > 0) {
const green = Math.floor(180 * intensity + 60);
return `rgb(0, ${green}, 0)`;
} else {
const red = Math.floor(180 * intensity + 60);
return `rgb(${red}, 0, 0)`;
}
};
const HeatmapCell: React.FC<{cell: HeatmapCell; period: '1h'|'24h'|'7d'}> = ({cell, period}) => {
const change = period === '1h' ? cell.change1h : period === '24h' ? cell.change24h : cell.change7d;
const bgColor = getColor(change);
return (
<div
style={{
position: 'absolute',
left: cell.x,
top: cell.y,
width: cell.width - 2,
height: cell.height - 2,
backgroundColor: bgColor,
border: '1px solid rgba(0,0,0,0.3)',
overflow: 'hidden',
cursor: 'pointer'
}}
title={`${cell.symbol}: ${change > 0 ? '+' : ''}${change.toFixed(2)}%`}
>
{cell.width > 40 && (
<div className="p-1 text-white">
<div className="font-bold text-xs">{cell.symbol}</div>
{cell.height > 30 && (
<div className={`text-xs ${change > 0 ? 'text-green-200' : 'text-red-200'}`}>
{change > 0 ? '+' : ''}{change.toFixed(2)}%
</div>
)}
</div>
)}
</div>
);
};
const CryptoHeatmap: React.FC = () => {
const [data, setData] = useState<HeatmapCell[]>([]);
const [period, setPeriod] = useState<'1h'|'24h'|'7d'>('24h');
const containerRef = useRef<HTMLDivElement>(null);
const [dimensions, setDimensions] = useState({width: 1200, height: 700});
const layoutData = useMemo(() => {
const treemap = new SquarifiedTreemap();
return treemap.layout(data, {x: 0, y: 0, ...dimensions});
}, [data, dimensions]);
return (
<div>
<div className="flex gap-2 mb-4">
{(['1h', '24h', '7d'] as const).map(p => (
<button key={p} onClick={() => setPeriod(p)}
className={`px-3 py-1 rounded ${period === p ? 'bg-blue-600' : 'bg-gray-700'}`}>
{p}
</button>
))}
</div>
<div ref={containerRef} className="relative bg-gray-900"
style={{width: dimensions.width, height: dimensions.height}}>
{layoutData.map(cell => (
<HeatmapCell key={cell.symbol} cell={cell} period={period} />
))}
</div>
</div>
);
};
Тепловая карта — это killer feature для криптоплатформы. Пользователи возвращаются к ней ежедневно для мониторинга рынка. Хорошая реализация включает: фильтрацию по секторам (DeFi, Layer1, GameFi), zoom на отдельные сектора, и clickthrough к торговой паре.







