Розробка візуалізації footprint-графіків

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

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

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

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

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

Розроблення Time & Sales (ленти сделок)

Time & Sales (T&S) — це безперервний потік усіх виконаних сделок у реальному часі: час, ціна, обсяг, напрям. Професійні трейдери читають T&S як пульс ринку — бачать агресію покупців та продавців, крупні блоки, послідовність ударів по bid/ask.

Структура ленти сделок

interface Trade {
  id: string;
  timestamp: number;         // unix мілісекунди
  price: number;
  quantity: number;
  side: 'buy' | 'sell';     // агресивна сторона
  value: number;             // price * quantity в USD
  isLargeTrade: boolean;     // вище порогу значимого обсягу
}

Кожен рядок T&S містить:

  • Час: HH:MM:SS.mmm (з мілісекундами для професійних платформ)
  • Ціна: з виділенням напрямку
  • Обсяг: у base asset
  • Сторона: Buy (зелений) / Sell (червоний)

Backend: агрегація та стриминг

type TimeAndSalesHub struct {
    trades     chan Trade
    clients    map[string]map[*WSClient]bool  // pair -> clients
    mu         sync.RWMutex
    recentBuf  map[string]*RingBuffer  // зберігає останні N сделок для нових підключень
}

type RingBuffer struct {
    items []Trade
    head  int
    size  int
    mu    sync.Mutex
}

func (rb *RingBuffer) Add(trade Trade) {
    rb.mu.Lock()
    defer rb.mu.Unlock()
    rb.items[rb.head%rb.size] = trade
    rb.head++
}

func (rb *RingBuffer) GetAll() []Trade {
    rb.mu.Lock()
    defer rb.mu.Unlock()
    
    result := make([]Trade, 0, rb.size)
    start := rb.head - rb.size
    if start < 0 { start = 0 }
    
    for i := start; i < rb.head; i++ {
        result = append(result, rb.items[i%rb.size])
    }
    return result
}

func (hub *TimeAndSalesHub) OnTrade(trade Trade) {
    // Зберігаємо в ring buffer
    hub.recentBuf[trade.Pair].Add(trade)
    
    // Broadcast всім підписчикам
    hub.mu.RLock()
    defer hub.mu.RUnlock()
    
    data, _ := json.Marshal(trade)
    for client := range hub.clients[trade.Pair] {
        select {
        case client.send <- data:
        default:
            go client.close()
        }
    }
}

// Новий клієнт отримує останні 100 сделок одразу
func (hub *TimeAndSalesHub) OnClientConnect(client *WSClient, pair string) {
    hub.mu.Lock()
    hub.clients[pair][client] = true
    hub.mu.Unlock()
    
    // Відправляємо історію
    recent := hub.recentBuf[pair].GetAll()
    for _, trade := range recent {
        data, _ := json.Marshal(trade)
        client.send <- data
    }
}

Frontend: високопродуктивний рендеринг

T&S оновлюється дуже часто — на BTC/USDT до 10–20 сделок на секунду в активні періоди. Стандартний React список буде тормозити.

import { useRef, useEffect, useCallback } from 'react';

const MAX_ROWS = 200;  // максимум рядків у ленті

function TimeAndSalesList({ pair }: { pair: string }) {
  const containerRef = useRef<HTMLDivElement>(null);
  const tradesRef = useRef<Trade[]>([]);
  const wsRef = useRef<WebSocket | null>(null);
  
  // Прямий DOM manipulation для hot path — без React re-render
  const appendTrade = useCallback((trade: Trade) => {
    const container = containerRef.current;
    if (!container) return;
    
    // Створюємо новий рядок
    const row = document.createElement('div');
    row.className = `trade-row ${trade.side} ${trade.isLargeTrade ? 'large' : ''}`;
    
    const time = new Date(trade.timestamp);
    const timeStr = `${time.getHours().toString().padStart(2,'0')}:` +
                    `${time.getMinutes().toString().padStart(2,'0')}:` +
                    `${time.getSeconds().toString().padStart(2,'0')}`;
    
    row.innerHTML = `
      <span class="time">${timeStr}</span>
      <span class="price">${formatPrice(trade.price)}</span>
      <span class="qty">${formatQuantity(trade.quantity)}</span>
      <span class="value">$${formatVolume(trade.value)}</span>
    `;
    
    // Вставляємо у початок (нові сделки зверху)
    container.insertBefore(row, container.firstChild);
    
    // Видаляємо зайві рядки внизу
    while (container.children.length > MAX_ROWS) {
      container.removeChild(container.lastChild!);
    }
    
    // Flash анімація для крупних сделок
    if (trade.isLargeTrade) {
      row.classList.add('flash');
      setTimeout(() => row.classList.remove('flash'), 500);
    }
  }, []);
  
  useEffect(() => {
    wsRef.current = new WebSocket(`wss://api.exchange.com/ws`);
    wsRef.current.send(JSON.stringify({ op: 'subscribe', channel: `trades.${pair}` }));
    
    wsRef.current.onmessage = (e) => {
      const trade = JSON.parse(e.data);
      appendTrade(trade);
    };
    
    return () => wsRef.current?.close();
  }, [pair, appendTrade]);
  
  return (
    <div className="time-and-sales">
      <div className="ts-header">
        <span>Time</span>
        <span>Price</span>
        <span>Size</span>
        <span>Value</span>
      </div>
      <div ref={containerRef} className="ts-body" />
    </div>
  );
}

Фільтрація та підсвітка

interface TSFilters {
  minValue: number;        // показувати тільки сделки >= $X
  side: 'all' | 'buy' | 'sell';
  highlightLargeThreshold: number;  // поріг для виділення крупних блоків
}

// CSS класи для стилізації
const styles = `
.trade-row {
  display: grid;
  grid-template-columns: 80px 100px 80px 80px;
  padding: 1px 8px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 12px;
  border-bottom: 1px solid #1e2030;
  transition: background-color 0.2s;
}

.trade-row.buy .price { color: #00B15E; }
.trade-row.sell .price { color: #E84242; }

.trade-row.large {
  background: rgba(255, 215, 0, 0.05);
  font-weight: 600;
}

.trade-row.large.buy { background: rgba(0, 177, 94, 0.15); }
.trade-row.large.sell { background: rgba(232, 66, 66, 0.15); }

@keyframes flash-green {
  0% { background-color: rgba(0, 177, 94, 0.4); }
  100% { background-color: transparent; }
}

.trade-row.flash { animation: flash-green 0.5s ease-out; }
`;

Агрегація по часу

Для менш щільних ринків — об'єднання сделок за короткий інтервал (100–500 мс):

class TradeAggregator {
  private buffer: Trade[] = [];
  private flushInterval: number = 100;  // ms
  private onFlush: (aggregated: AggregatedTrade[]) => void;
  
  add(trade: Trade) {
    this.buffer.push(trade);
  }
  
  private flush() {
    if (this.buffer.length === 0) return;
    
    // Групуємо по цені (тік) та стороні
    const groups = new Map<string, AggregatedTrade>();
    
    for (const trade of this.buffer) {
      const key = `${trade.price}:${trade.side}`;
      const existing = groups.get(key);
      
      if (existing) {
        existing.quantity += trade.quantity;
        existing.value += trade.value;
        existing.count++;
      } else {
        groups.set(key, { ...trade, count: 1 });
      }
    }
    
    this.onFlush([...groups.values()].sort((a, b) => b.timestamp - a.timestamp));
    this.buffer = [];
  }
}

Статистика потоку сделок

Корисно показувати агрегаційну статистику поруч з лентою для останніх N секунд:

function TradeFlowStats({ trades }: { trades: Trade[] }) {
  const stats = useMemo(() => {
    const last60s = trades.filter(t => Date.now() - t.timestamp < 60000);
    
    const buyVolume = last60s.filter(t => t.side === 'buy')
                             .reduce((sum, t) => sum + t.value, 0);
    const sellVolume = last60s.filter(t => t.side === 'sell')
                              .reduce((sum, t) => sum + t.value, 0);
    
    return {
      buyVolume,
      sellVolume,
      delta: buyVolume - sellVolume,
      buyPercent: buyVolume / (buyVolume + sellVolume) * 100,
    };
  }, [trades]);
  
  return (
    <div className="flow-stats">
      <span className="green">Buy: ${formatVolume(stats.buyVolume)}</span>
      <FlowBar buyPct={stats.buyPercent} />
      <span className="red">Sell: ${formatVolume(stats.sellVolume)}</span>
      <span className={stats.delta > 0 ? 'green' : 'red'}>
        Δ {stats.delta > 0 ? '+' : ''}${formatVolume(Math.abs(stats.delta))}
      </span>
    </div>
  );
}

Розроблення Time & Sales ленти з real-time WebSocket оновленнями, фільтрацією, підсвіткою крупних блоків та flow статистикою: 3–4 тижні.