trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/especificaciones/ET-TRD-008-performance.md
rckrdmrd a7cca885f0 feat: Major platform documentation and architecture updates
Changes include:
- Updated architecture documentation
- Enhanced module definitions (OQI-001 to OQI-008)
- ML integration documentation updates
- Trading strategies documentation
- Orchestration and inventory updates
- Docker configuration updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 05:33:35 -06:00

29 KiB

id title type status priority epic project version created_date updated_date
ET-TRD-008 Especificación Técnica - Performance Optimizations Technical Specification Done Alta OQI-003 trading-platform 1.0.0 2025-12-05 2026-01-04

ET-TRD-008: Especificación Técnica - Performance Optimizations

Version: 1.0.0 Fecha: 2025-12-05 Estado: Pendiente Épica: OQI-003 Requerimiento: RF-TRD-008


Resumen

Esta especificación detalla las optimizaciones de performance para el módulo de trading, incluyendo Web Workers para cálculos pesados, virtualización de listas, caché estratégico, lazy loading de componentes y optimización de re-renders.


Arquitectura

┌─────────────────────────────────────────────────────────────────────────┐
│                         FRONTEND OPTIMIZATIONS                           │
│                                                                          │
│  ┌──────────────────────────────────────────────────────────────────┐   │
│  │                    Component Layer                               │   │
│  │  ┌────────────────┐  ┌──────────────────┐  ┌────────────────┐  │   │
│  │  │ React.memo()   │  │ useMemo()        │  │ useCallback()  │  │   │
│  │  │ Components     │  │ Computations     │  │ Functions      │  │   │
│  │  └────────────────┘  └──────────────────┘  └────────────────┘  │   │
│  └──────────────────────────────────────────────────────────────────┘   │
│                                │                                         │
│                                ▼                                         │
│  ┌──────────────────────────────────────────────────────────────────┐   │
│  │                    Virtualization Layer                          │   │
│  │  ┌────────────────┐  ┌──────────────────┐  ┌────────────────┐  │   │
│  │  │ react-window   │  │ Intersection     │  │ Infinite       │  │   │
│  │  │ Virtual Lists  │  │ Observer         │  │ Scroll         │  │   │
│  │  └────────────────┘  └──────────────────┘  └────────────────┘  │   │
│  └──────────────────────────────────────────────────────────────────┘   │
│                                │                                         │
│                                ▼                                         │
│  ┌──────────────────────────────────────────────────────────────────┐   │
│  │                    Web Workers                                   │   │
│  │  ┌────────────────┐  ┌──────────────────┐  ┌────────────────┐  │   │
│  │  │ Indicator      │  │ Data             │  │ Chart          │  │   │
│  │  │ Calculations   │  │ Processing       │  │ Rendering      │  │   │
│  │  └────────────────┘  └──────────────────┘  └────────────────┘  │   │
│  └──────────────────────────────────────────────────────────────────┘   │
│                                │                                         │
│                                ▼                                         │
│  ┌──────────────────────────────────────────────────────────────────┐   │
│  │                    Cache Layer                                   │   │
│  │  ┌────────────────┐  ┌──────────────────┐  ┌────────────────┐  │   │
│  │  │ IndexedDB      │  │ Memory Cache     │  │ Service        │  │   │
│  │  │ Storage        │  │ (Map/LRU)        │  │ Worker         │  │   │
│  │  └────────────────┘  └──────────────────┘  └────────────────┘  │   │
│  └──────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                      BACKEND OPTIMIZATIONS                               │
│  ┌──────────────────────────────────────────────────────────────────┐   │
│  │  ┌────────────────┐  ┌──────────────────┐  ┌────────────────┐  │   │
│  │  │ Redis Cache    │  │ Database         │  │ Query          │  │   │
│  │  │ (Hot Data)     │  │ Indexing         │  │ Optimization   │  │   │
│  │  └────────────────┘  └──────────────────┘  └────────────────┘  │   │
│  └──────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘

1. Web Workers

Indicator Calculation Worker

Ubicación: apps/frontend/src/modules/trading/workers/indicator.worker.ts

import { IndicatorService } from '../services/indicator.service';

interface WorkerMessage {
  id: string;
  type: 'calculate' | 'batch';
  indicatorType?: string;
  data?: any[];
  params?: any;
  batch?: Array<{
    indicatorType: string;
    data: any[];
    params: any;
  }>;
}

interface WorkerResponse {
  id: string;
  success: boolean;
  data?: any;
  error?: string;
}

// Handle messages from main thread
self.onmessage = (event: MessageEvent<WorkerMessage>) => {
  const { id, type, indicatorType, data, params, batch } = event.data;

  try {
    if (type === 'calculate') {
      const result = IndicatorService.calculate(indicatorType!, data!, params!);

      self.postMessage({
        id,
        success: true,
        data: result,
      } as WorkerResponse);
    } else if (type === 'batch') {
      const results = batch!.map((item) =>
        IndicatorService.calculate(item.indicatorType, item.data, item.params)
      );

      self.postMessage({
        id,
        success: true,
        data: results,
      } as WorkerResponse);
    }
  } catch (error: any) {
    self.postMessage({
      id,
      success: false,
      error: error.message,
    } as WorkerResponse);
  }
};

Worker Hook

Ubicación: apps/frontend/src/modules/trading/hooks/useWorker.ts

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

interface WorkerPool {
  workers: Worker[];
  queue: Array<{
    id: string;
    message: any;
    resolve: (value: any) => void;
    reject: (error: any) => void;
  }>;
  activeWorkers: number;
}

export function useWorkerPool(workerCount: number = 4) {
  const poolRef = useRef<WorkerPool>({
    workers: [],
    queue: [],
    activeWorkers: 0,
  });

  const pendingRequests = useRef<Map<string, {
    resolve: (value: any) => void;
    reject: (error: any) => void;
  }>>(new Map());

  // Initialize worker pool
  useEffect(() => {
    for (let i = 0; i < workerCount; i++) {
      const worker = new Worker(
        new URL('../workers/indicator.worker.ts', import.meta.url),
        { type: 'module' }
      );

      worker.onmessage = (event) => {
        const { id, success, data, error } = event.data;
        const request = pendingRequests.current.get(id);

        if (request) {
          if (success) {
            request.resolve(data);
          } else {
            request.reject(new Error(error));
          }
          pendingRequests.current.delete(id);
        }

        poolRef.current.activeWorkers--;
        processQueue();
      };

      worker.onerror = (error) => {
        console.error('Worker error:', error);
      };

      poolRef.current.workers.push(worker);
    }

    return () => {
      poolRef.current.workers.forEach((worker) => worker.terminate());
    };
  }, [workerCount]);

  const processQueue = useCallback(() => {
    const pool = poolRef.current;

    while (
      pool.queue.length > 0 &&
      pool.activeWorkers < pool.workers.length
    ) {
      const task = pool.queue.shift()!;
      const workerIndex = pool.activeWorkers % pool.workers.length;
      const worker = pool.workers[workerIndex];

      pendingRequests.current.set(task.id, {
        resolve: task.resolve,
        reject: task.reject,
      });

      worker.postMessage(task.message);
      pool.activeWorkers++;
    }
  }, []);

  const execute = useCallback(<T = any>(message: any): Promise<T> => {
    return new Promise((resolve, reject) => {
      const id = `${Date.now()}_${Math.random()}`;
      const task = {
        id,
        message: { ...message, id },
        resolve,
        reject,
      };

      poolRef.current.queue.push(task);
      processQueue();
    });
  }, [processQueue]);

  return { execute };
}

Uso:

function ChartComponent() {
  const { execute } = useWorkerPool(4);
  const { klines } = useTradingStore();

  const calculateIndicators = useCallback(async () => {
    const results = await execute({
      type: 'batch',
      batch: [
        {
          indicatorType: 'SMA',
          data: klines,
          params: { period: 20 },
        },
        {
          indicatorType: 'RSI',
          data: klines,
          params: { period: 14 },
        },
      ],
    });

    return results;
  }, [klines, execute]);

  // Use results...
}

2. Virtualización de Listas

Order List con react-window

Ubicación: apps/frontend/src/modules/trading/components/VirtualizedOrderList.tsx

import React from 'react';
import { FixedSizeList as List } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
import { PaperOrder } from '../types';

interface Props {
  orders: PaperOrder[];
  onOrderClick?: (order: PaperOrder) => void;
}

export const VirtualizedOrderList: React.FC<Props> = ({
  orders,
  onOrderClick,
}) => {
  const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => {
    const order = orders[index];

    return (
      <div
        style={style}
        className="border-b border-gray-700 hover:bg-gray-750 cursor-pointer px-4"
        onClick={() => onOrderClick?.(order)}
      >
        <OrderRow order={order} />
      </div>
    );
  };

  return (
    <AutoSizer>
      {({ height, width }) => (
        <List
          height={height}
          itemCount={orders.length}
          itemSize={80}
          width={width}
          overscanCount={5} // Render 5 extra items above and below viewport
        >
          {Row}
        </List>
      )}
    </AutoSizer>
  );
};

const OrderRow = React.memo(({ order }: { order: PaperOrder }) => {
  return (
    <div className="flex items-center justify-between py-3">
      <div>
        <div className="font-semibold text-white">{order.symbol}</div>
        <div className="text-xs text-gray-400">
          {order.side.toUpperCase()} {order.type}
        </div>
      </div>
      <div className="text-right">
        <div className="text-white">{order.quantity}</div>
        <div
          className={`text-xs ${
            order.status === 'filled'
              ? 'text-green-500'
              : order.status === 'cancelled'
              ? 'text-red-500'
              : 'text-yellow-500'
          }`}
        >
          {order.status}
        </div>
      </div>
    </div>
  );
});

Infinite Scroll para Trades

import React, { useRef, useCallback } from 'react';
import { useInfiniteQuery } from '@tanstack/react-query';
import { api } from '../services/api';

export const InfiniteTradeList: React.FC = () => {
  const observerRef = useRef<IntersectionObserver>();
  const lastElementRef = useRef<HTMLDivElement>(null);

  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['trades'],
    queryFn: ({ pageParam = 0 }) =>
      api.get('/paper/trades', {
        params: { limit: 50, offset: pageParam },
      }),
    getNextPageParam: (lastPage, pages) => {
      if (lastPage.data.pagination.hasMore) {
        return pages.length * 50;
      }
      return undefined;
    },
  });

  // Intersection Observer para infinite scroll
  const lastElementObserver = useCallback(
    (node: HTMLDivElement | null) => {
      if (isFetchingNextPage) return;

      if (observerRef.current) {
        observerRef.current.disconnect();
      }

      observerRef.current = new IntersectionObserver((entries) => {
        if (entries[0].isIntersecting && hasNextPage) {
          fetchNextPage();
        }
      });

      if (node) {
        observerRef.current.observe(node);
      }
    },
    [isFetchingNextPage, fetchNextPage, hasNextPage]
  );

  const trades = data?.pages.flatMap((page) => page.data.data) ?? [];

  return (
    <div className="overflow-y-auto h-full">
      {trades.map((trade, index) => (
        <div
          key={trade.id}
          ref={index === trades.length - 1 ? lastElementObserver : null}
          className="border-b border-gray-700 p-3"
        >
          <TradeRow trade={trade} />
        </div>
      ))}
      {isFetchingNextPage && (
        <div className="text-center py-4">Loading more...</div>
      )}
    </div>
  );
};

3. Cache Estratégico

IndexedDB Cache

Ubicación: apps/frontend/src/modules/trading/services/indexeddb-cache.ts

import { openDB, DBSchema, IDBPDatabase } from 'idb';

interface TradingDB extends DBSchema {
  klines: {
    key: string; // symbol:interval
    value: {
      symbol: string;
      interval: string;
      data: any[];
      timestamp: number;
    };
  };
  symbols: {
    key: string;
    value: {
      data: any[];
      timestamp: number;
    };
  };
}

class IndexedDBCache {
  private db: IDBPDatabase<TradingDB> | null = null;
  private readonly DB_NAME = 'trading-cache';
  private readonly DB_VERSION = 1;

  async initialize() {
    if (this.db) return;

    this.db = await openDB<TradingDB>(this.DB_NAME, this.DB_VERSION, {
      upgrade(db) {
        // Create object stores
        if (!db.objectStoreNames.contains('klines')) {
          db.createObjectStore('klines', { keyPath: 'key' });
        }
        if (!db.objectStoreNames.contains('symbols')) {
          db.createObjectStore('symbols', { keyPath: 'key' });
        }
      },
    });
  }

  async getKlines(symbol: string, interval: string) {
    if (!this.db) await this.initialize();

    const key = `${symbol}:${interval}`;
    const cached = await this.db!.get('klines', key);

    if (!cached) return null;

    // Check if cache is still valid (5 minutes)
    const now = Date.now();
    const cacheAge = now - cached.timestamp;
    const maxAge = 5 * 60 * 1000; // 5 minutes

    if (cacheAge > maxAge) {
      await this.db!.delete('klines', key);
      return null;
    }

    return cached.data;
  }

  async setKlines(symbol: string, interval: string, data: any[]) {
    if (!this.db) await this.initialize();

    const key = `${symbol}:${interval}`;
    await this.db!.put('klines', {
      key,
      symbol,
      interval,
      data,
      timestamp: Date.now(),
    });
  }

  async clearExpired() {
    if (!this.db) await this.initialize();

    const now = Date.now();
    const maxAge = 5 * 60 * 1000;

    // Clear expired klines
    const tx = this.db!.transaction('klines', 'readwrite');
    const store = tx.objectStore('klines');
    const allKeys = await store.getAllKeys();

    for (const key of allKeys) {
      const item = await store.get(key);
      if (item && now - item.timestamp > maxAge) {
        await store.delete(key);
      }
    }

    await tx.done;
  }
}

export const indexedDBCache = new IndexedDBCache();

Memory Cache (LRU)

// services/lru-cache.ts

class LRUCache<K, V> {
  private cache: Map<K, V>;
  private maxSize: number;

  constructor(maxSize: number = 100) {
    this.cache = new Map();
    this.maxSize = maxSize;
  }

  get(key: K): V | undefined {
    const value = this.cache.get(key);
    if (value !== undefined) {
      // Move to end (most recently used)
      this.cache.delete(key);
      this.cache.set(key, value);
    }
    return value;
  }

  set(key: K, value: V): void {
    // Delete if exists to re-add at end
    this.cache.delete(key);

    // Add to end
    this.cache.set(key, value);

    // Remove oldest if over capacity
    if (this.cache.size > this.maxSize) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
  }

  has(key: K): boolean {
    return this.cache.has(key);
  }

  clear(): void {
    this.cache.clear();
  }

  get size(): number {
    return this.cache.size;
  }
}

export const tickerCache = new LRUCache<string, any>(50);
export const orderBookCache = new LRUCache<string, any>(20);

4. Component Optimizations

React.memo y useMemo

// components/OptimizedOrderPanel.tsx

import React, { useMemo, useCallback } from 'react';

export const OptimizedOrderPanel = React.memo(({ symbol, ticker, balance }) => {
  // Memoize expensive calculations
  const estimatedCost = useMemo(() => {
    return calculateEstimatedCost(orderQuantity, orderPrice, ticker?.price);
  }, [orderQuantity, orderPrice, ticker?.price]);

  const maxQuantity = useMemo(() => {
    return calculateMaxQuantity(balance.available, ticker?.price);
  }, [balance.available, ticker?.price]);

  // Memoize callbacks
  const handleSubmit = useCallback(
    async (e: React.FormEvent) => {
      e.preventDefault();
      // Submit logic
    },
    [orderQuantity, orderPrice, symbol]
  );

  return (
    <div>
      {/* Component JSX */}
    </div>
  );
}, (prevProps, nextProps) => {
  // Custom comparison
  return (
    prevProps.symbol === nextProps.symbol &&
    prevProps.ticker?.price === nextProps.ticker?.price &&
    prevProps.balance.available === nextProps.balance.available
  );
});

Debouncing e Input Throttling

// hooks/useDebounce.ts

import { useEffect, useState } from 'react';

export function useDebounce<T>(value: T, delay: number = 300): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

// Usage
function SearchSymbol() {
  const [search, setSearch] = useState('');
  const debouncedSearch = useDebounce(search, 500);

  useEffect(() => {
    if (debouncedSearch) {
      // API call only after 500ms of no typing
      searchSymbols(debouncedSearch);
    }
  }, [debouncedSearch]);

  return (
    <input
      value={search}
      onChange={(e) => setSearch(e.target.value)}
      placeholder="Search symbols..."
    />
  );
}

5. Lazy Loading

Code Splitting

// pages/TradingPage.tsx

import React, { lazy, Suspense } from 'react';

// Lazy load heavy components
const ChartComponent = lazy(() => import('../components/ChartComponent'));
const OrderBookPanel = lazy(() => import('../components/OrderBookPanel'));
const PositionsPanel = lazy(() => import('../components/PositionsPanel'));

export const TradingPage: React.FC = () => {
  return (
    <div className="trading-page">
      <Suspense fallback={<ChartSkeleton />}>
        <ChartComponent />
      </Suspense>

      <div className="panels">
        <Suspense fallback={<PanelSkeleton />}>
          <OrderBookPanel />
        </Suspense>

        <Suspense fallback={<PanelSkeleton />}>
          <PositionsPanel />
        </Suspense>
      </div>
    </div>
  );
};

Dynamic Imports

// Lazy load indicators
async function loadIndicator(type: string) {
  switch (type) {
    case 'SMA':
      return (await import('../indicators/sma')).calculateSMA;
    case 'RSI':
      return (await import('../indicators/rsi')).calculateRSI;
    case 'MACD':
      return (await import('../indicators/macd')).calculateMACD;
    default:
      throw new Error(`Unknown indicator: ${type}`);
  }
}

6. Backend Optimizations

Database Query Optimization

-- Create covering indexes for common queries
CREATE INDEX idx_paper_orders_user_status_symbol
ON trading.paper_orders(user_id, status, symbol)
INCLUDE (quantity, price, placed_at);

-- Materialized view for position summary
CREATE MATERIALIZED VIEW trading.position_summary AS
SELECT
    user_id,
    symbol,
    COUNT(*) as total_positions,
    SUM(CASE WHEN status = 'open' THEN 1 ELSE 0 END) as open_positions,
    SUM(realized_pnl) as total_realized_pnl,
    SUM(unrealized_pnl) as total_unrealized_pnl
FROM trading.paper_positions
GROUP BY user_id, symbol;

-- Refresh periodically
CREATE OR REPLACE FUNCTION refresh_position_summary()
RETURNS void AS $$
BEGIN
    REFRESH MATERIALIZED VIEW CONCURRENTLY trading.position_summary;
END;
$$ LANGUAGE plpgsql;

Connection Pooling

// db/pool.ts

import { Pool } from 'pg';

export const pool = new Pool({
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT || '5432'),
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  max: 20, // Maximum pool size
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

// Prepared statements for frequently used queries
export const preparedStatements = {
  getBalance: {
    name: 'get-balance',
    text: 'SELECT * FROM trading.paper_balances WHERE user_id = $1 AND asset = $2',
  },
  getOpenPosition: {
    name: 'get-open-position',
    text: `
      SELECT * FROM trading.paper_positions
      WHERE user_id = $1 AND symbol = $2 AND status = 'open'
      LIMIT 1
    `,
  },
};

7. Performance Monitoring

React DevTools Profiler

import { Profiler, ProfilerOnRenderCallback } from 'react';

const onRenderCallback: ProfilerOnRenderCallback = (
  id,
  phase,
  actualDuration,
  baseDuration,
  startTime,
  commitTime
) => {
  if (actualDuration > 16) {
    // Log slow renders (> 1 frame at 60fps)
    console.warn(`Slow render in ${id}:`, {
      phase,
      actualDuration,
      baseDuration,
    });
  }
};

export function TradingPageWithProfiler() {
  return (
    <Profiler id="TradingPage" onRender={onRenderCallback}>
      <TradingPage />
    </Profiler>
  );
}

Performance Metrics

// services/performance.ts

export class PerformanceMonitor {
  static measureRender(componentName: string, callback: () => void) {
    const startTime = performance.now();
    callback();
    const endTime = performance.now();

    const duration = endTime - startTime;

    if (duration > 16) {
      console.warn(`${componentName} took ${duration.toFixed(2)}ms to render`);
    }

    return duration;
  }

  static measureAsync(name: string, promise: Promise<any>) {
    const startTime = performance.now();

    return promise.finally(() => {
      const endTime = performance.now();
      const duration = endTime - startTime;

      console.log(`${name} took ${duration.toFixed(2)}ms`);
    });
  }

  static markNavigation(name: string) {
    performance.mark(name);
  }

  static measureNavigation(start: string, end: string) {
    performance.measure(`${start} -> ${end}`, start, end);
    const measure = performance.getEntriesByName(`${start} -> ${end}`)[0];
    console.log(`Navigation took ${measure.duration.toFixed(2)}ms`);
  }
}

8. Bundle Optimization

Webpack Configuration

// webpack.config.js

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        // Vendor libraries
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10,
        },
        // Trading module
        trading: {
          test: /[\\/]src[\\/]modules[\\/]trading[\\/]/,
          name: 'trading',
          priority: 5,
        },
        // Lightweight Charts (large library)
        charts: {
          test: /[\\/]node_modules[\\/]lightweight-charts[\\/]/,
          name: 'charts',
          priority: 15,
        },
      },
    },
    runtimeChunk: 'single',
  },
};

Performance Checklist

## Frontend Performance

- [x] Use React.memo for pure components
- [x] Implement useMemo for expensive calculations
- [x] Use useCallback for event handlers
- [x] Virtualize long lists (react-window)
- [x] Lazy load routes and heavy components
- [x] Implement Web Workers for calculations
- [x] Use IndexedDB for offline caching
- [x] Debounce user inputs
- [x] Optimize bundle size (code splitting)
- [x] Use production builds

## Backend Performance

- [x] Database indexing on frequent queries
- [x] Connection pooling
- [x] Redis caching for hot data
- [x] Prepared statements
- [x] Query result pagination
- [x] Materialized views for aggregations
- [x] Rate limiting
- [x] Compression (gzip/brotli)

## Monitoring

- [x] React Profiler for render times
- [x] Performance.mark() for navigation
- [x] API response time logging
- [x] Error tracking (Sentry)
- [x] Web Vitals tracking

Testing

describe('Performance Tests', () => {
  it('should render large order list in under 100ms', () => {
    const orders = generateMockOrders(1000);
    const startTime = performance.now();

    render(<VirtualizedOrderList orders={orders} />);

    const endTime = performance.now();
    expect(endTime - startTime).toBeLessThan(100);
  });

  it('should calculate indicators in Web Worker', async () => {
    const klines = generateMockKlines(1000);
    const { execute } = renderHook(() => useWorkerPool(1)).result.current;

    const startTime = performance.now();

    await execute({
      type: 'calculate',
      indicatorType: 'SMA',
      data: klines,
      params: { period: 20 },
    });

    const endTime = performance.now();
    expect(endTime - startTime).toBeLessThan(50);
  });
});

Referencias