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>
29 KiB
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);
});
});