1007 lines
29 KiB
Markdown
1007 lines
29 KiB
Markdown
# ET-TRD-008: Especificación Técnica - Performance Optimizations
|
|
|
|
**Version:** 1.0.0
|
|
**Fecha:** 2025-12-05
|
|
**Estado:** Pendiente
|
|
**Épica:** [OQI-003](../_MAP.md)
|
|
**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`
|
|
|
|
```typescript
|
|
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`
|
|
|
|
```typescript
|
|
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:**
|
|
|
|
```typescript
|
|
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`
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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`
|
|
|
|
```typescript
|
|
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)
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```sql
|
|
-- 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```markdown
|
|
## 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
|
|
|
|
```typescript
|
|
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
|
|
|
|
- [React Performance Optimization](https://react.dev/learn/render-and-commit)
|
|
- [Web Workers API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API)
|
|
- [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
|
|
- [react-window Documentation](https://github.com/bvaughn/react-window)
|
|
- [PostgreSQL Performance Tips](https://wiki.postgresql.org/wiki/Performance_Optimization)
|