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>
34 KiB
| id | title | type | status | priority | epic | project | version | created_date | updated_date |
|---|---|---|---|---|---|---|---|---|---|
| ET-TRD-005 | Especificación Técnica - Frontend Components | Technical Specification | Done | Alta | OQI-003 | trading-platform | 1.0.0 | 2025-12-05 | 2026-01-04 |
ET-TRD-005: Especificación Técnica - Frontend Components
Version: 1.0.0 Fecha: 2025-12-05 Estado: Pendiente Épica: OQI-003 Requerimiento: RF-TRD-005
Resumen
Esta especificación detalla la implementación técnica de los componentes React para el módulo de trading, incluyendo TradingPage, ChartComponent (Lightweight Charts), OrderPanel, PositionsPanel, WatchlistPanel y gestión de estado con Zustand.
Arquitectura
┌─────────────────────────────────────────────────────────────────────────┐
│ TRADING PAGE │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Layout │ │
│ │ ┌────────────────┐ ┌─────────────────────────────────────┐ │ │
│ │ │ │ │ │ │ │
│ │ │ Watchlist │ │ Chart Component │ │ │
│ │ │ Panel │ │ (Lightweight Charts v4) │ │ │
│ │ │ │ │ │ │ │
│ │ │ - Symbols │ │ - Candlesticks │ │ │
│ │ │ - Search │ │ - Indicators │ │ │
│ │ │ - Alerts │ │ - Drawings │ │ │
│ │ │ │ │ - Timeframes │ │ │
│ │ └────────────────┘ └─────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │
│ │ │ Order Panel │ │ Positions │ │ Order Book │ │ │
│ │ │ │ │ Panel │ │ Panel │ │ │
│ │ │ - Buy/Sell │ │ │ │ │ │ │
│ │ │ - Order Type │ │ - Open │ │ - Bids/Asks │ │ │
│ │ │ - Amount │ │ - History │ │ - Depth │ │ │
│ │ │ - SL/TP │ │ - PnL │ │ │ │ │
│ │ └────────────────┘ └──────────────┘ └──────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ ZUSTAND STORES │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ tradingStore │ │ orderStore │ │ chartStore │ │
│ │ │ │ │ │ │ │
│ │ - symbol │ │ - orders │ │ - interval │ │
│ │ - ticker │ │ - positions │ │ - indicators │ │
│ │ - orderbook │ │ - balances │ │ - drawings │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ SERVICES & HOOKS │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ useWebSocket │ │ useMarketData│ │ useOrders │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
Stores - Zustand
tradingStore
Ubicación: apps/frontend/src/modules/trading/stores/trading.store.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
export interface Ticker {
symbol: string;
price: number;
priceChange: number;
priceChangePercent: number;
high: number;
low: number;
volume: number;
}
export interface OrderBook {
lastUpdateId: number;
bids: [number, number][];
asks: [number, number][];
}
interface TradingState {
// Selected symbol
selectedSymbol: string;
setSelectedSymbol: (symbol: string) => void;
// Market data
ticker: Ticker | null;
setTicker: (ticker: Ticker) => void;
orderBook: OrderBook | null;
setOrderBook: (orderBook: OrderBook) => void;
// Klines/Candles
klines: any[];
setKlines: (klines: any[]) => void;
appendKline: (kline: any) => void;
// Watchlists
watchlists: any[];
setWatchlists: (watchlists: any[]) => void;
selectedWatchlistId: string | null;
setSelectedWatchlistId: (id: string | null) => void;
// Loading states
isLoadingTicker: boolean;
isLoadingKlines: boolean;
isLoadingOrderBook: boolean;
// Actions
reset: () => void;
}
export const useTradingStore = create<TradingState>()(
devtools(
persist(
(set, get) => ({
// Initial state
selectedSymbol: 'BTCUSDT',
ticker: null,
orderBook: null,
klines: [],
watchlists: [],
selectedWatchlistId: null,
isLoadingTicker: false,
isLoadingKlines: false,
isLoadingOrderBook: false,
// Actions
setSelectedSymbol: (symbol) => set({ selectedSymbol: symbol }),
setTicker: (ticker) => set({ ticker, isLoadingTicker: false }),
setOrderBook: (orderBook) => set({ orderBook, isLoadingOrderBook: false }),
setKlines: (klines) => set({ klines, isLoadingKlines: false }),
appendKline: (kline) => {
const { klines } = get();
const lastKline = klines[klines.length - 1];
// Update last kline if same timestamp, otherwise append
if (lastKline && lastKline.openTime === kline.openTime) {
set({
klines: [...klines.slice(0, -1), kline],
});
} else {
set({
klines: [...klines, kline],
});
}
},
setWatchlists: (watchlists) => set({ watchlists }),
setSelectedWatchlistId: (id) => set({ selectedWatchlistId: id }),
reset: () =>
set({
ticker: null,
orderBook: null,
klines: [],
isLoadingTicker: false,
isLoadingKlines: false,
isLoadingOrderBook: false,
}),
}),
{
name: 'trading-storage',
partialize: (state) => ({
selectedSymbol: state.selectedSymbol,
selectedWatchlistId: state.selectedWatchlistId,
}),
}
)
)
);
orderStore
Ubicación: apps/frontend/src/modules/trading/stores/order.store.ts
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
export interface PaperBalance {
asset: string;
total: number;
available: number;
locked: number;
}
export interface PaperOrder {
id: string;
symbol: string;
side: 'buy' | 'sell';
type: 'market' | 'limit' | 'stop_loss' | 'stop_limit';
status: 'pending' | 'open' | 'filled' | 'cancelled';
quantity: number;
filledQuantity: number;
price?: number;
averageFillPrice?: number;
placedAt: Date;
}
export interface PaperPosition {
id: string;
symbol: string;
side: 'long' | 'short';
status: 'open' | 'closed';
currentQuantity: number;
averageEntryPrice: number;
unrealizedPnl: number;
realizedPnl: number;
totalPnl: number;
pnlPercentage: number;
openedAt: Date;
}
interface OrderState {
// Balances
balances: PaperBalance[];
setBalances: (balances: PaperBalance[]) => void;
getBalance: (asset: string) => PaperBalance | undefined;
// Orders
orders: PaperOrder[];
setOrders: (orders: PaperOrder[]) => void;
addOrder: (order: PaperOrder) => void;
updateOrder: (orderId: string, updates: Partial<PaperOrder>) => void;
removeOrder: (orderId: string) => void;
// Positions
positions: PaperPosition[];
setPositions: (positions: PaperPosition[]) => void;
updatePosition: (positionId: string, updates: Partial<PaperPosition>) => void;
// Order form state
orderSide: 'buy' | 'sell';
setOrderSide: (side: 'buy' | 'sell') => void;
orderType: 'market' | 'limit' | 'stop_loss';
setOrderType: (type: 'market' | 'limit' | 'stop_loss') => void;
orderQuantity: string;
setOrderQuantity: (quantity: string) => void;
orderPrice: string;
setOrderPrice: (price: string) => void;
stopLoss: string;
setStopLoss: (price: string) => void;
takeProfit: string;
setTakeProfit: (price: string) => void;
// Actions
resetOrderForm: () => void;
}
export const useOrderStore = create<OrderState>()(
devtools((set, get) => ({
// Initial state
balances: [],
orders: [],
positions: [],
orderSide: 'buy',
orderType: 'market',
orderQuantity: '',
orderPrice: '',
stopLoss: '',
takeProfit: '',
// Actions
setBalances: (balances) => set({ balances }),
getBalance: (asset) => {
return get().balances.find((b) => b.asset === asset);
},
setOrders: (orders) => set({ orders }),
addOrder: (order) =>
set((state) => ({
orders: [order, ...state.orders],
})),
updateOrder: (orderId, updates) =>
set((state) => ({
orders: state.orders.map((o) =>
o.id === orderId ? { ...o, ...updates } : o
),
})),
removeOrder: (orderId) =>
set((state) => ({
orders: state.orders.filter((o) => o.id !== orderId),
})),
setPositions: (positions) => set({ positions }),
updatePosition: (positionId, updates) =>
set((state) => ({
positions: state.positions.map((p) =>
p.id === positionId ? { ...p, ...updates } : p
),
})),
setOrderSide: (side) => set({ orderSide: side }),
setOrderType: (type) => set({ orderType: type }),
setOrderQuantity: (quantity) => set({ orderQuantity: quantity }),
setOrderPrice: (price) => set({ orderPrice: price }),
setStopLoss: (price) => set({ stopLoss: price }),
setTakeProfit: (price) => set({ takeProfit: price }),
resetOrderForm: () =>
set({
orderQuantity: '',
orderPrice: '',
stopLoss: '',
takeProfit: '',
}),
}))
);
chartStore
Ubicación: apps/frontend/src/modules/trading/stores/chart.store.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
export type TimeInterval = '1m' | '5m' | '15m' | '1h' | '4h' | '1d';
export interface ChartIndicator {
id: string;
type: 'SMA' | 'EMA' | 'RSI' | 'MACD' | 'BB';
params: Record<string, any>;
visible: boolean;
color?: string;
}
interface ChartState {
// Timeframe
interval: TimeInterval;
setInterval: (interval: TimeInterval) => void;
// Chart type
chartType: 'candlestick' | 'line' | 'area';
setChartType: (type: 'candlestick' | 'line' | 'area') => void;
// Indicators
indicators: ChartIndicator[];
addIndicator: (indicator: ChartIndicator) => void;
removeIndicator: (id: string) => void;
updateIndicator: (id: string, updates: Partial<ChartIndicator>) => void;
toggleIndicator: (id: string) => void;
// Drawings
drawingsEnabled: boolean;
setDrawingsEnabled: (enabled: boolean) => void;
// Settings
showVolume: boolean;
toggleVolume: () => void;
showGrid: boolean;
toggleGrid: () => void;
theme: 'light' | 'dark';
setTheme: (theme: 'light' | 'dark') => void;
}
export const useChartStore = create<ChartState>()(
devtools(
persist(
(set, get) => ({
// Initial state
interval: '1h',
chartType: 'candlestick',
indicators: [],
drawingsEnabled: false,
showVolume: true,
showGrid: true,
theme: 'dark',
// Actions
setInterval: (interval) => set({ interval }),
setChartType: (chartType) => set({ chartType }),
addIndicator: (indicator) =>
set((state) => ({
indicators: [...state.indicators, indicator],
})),
removeIndicator: (id) =>
set((state) => ({
indicators: state.indicators.filter((i) => i.id !== id),
})),
updateIndicator: (id, updates) =>
set((state) => ({
indicators: state.indicators.map((i) =>
i.id === id ? { ...i, ...updates } : i
),
})),
toggleIndicator: (id) =>
set((state) => ({
indicators: state.indicators.map((i) =>
i.id === id ? { ...i, visible: !i.visible } : i
),
})),
setDrawingsEnabled: (enabled) => set({ drawingsEnabled: enabled }),
toggleVolume: () => set((state) => ({ showVolume: !state.showVolume })),
toggleGrid: () => set((state) => ({ showGrid: !state.showGrid })),
setTheme: (theme) => set({ theme }),
}),
{
name: 'chart-storage',
}
)
)
);
Componentes Principales
TradingPage
Ubicación: apps/frontend/src/modules/trading/pages/TradingPage.tsx
import React, { useEffect } from 'react';
import { WatchlistPanel } from '../components/WatchlistPanel';
import { ChartComponent } from '../components/ChartComponent';
import { OrderPanel } from '../components/OrderPanel';
import { PositionsPanel } from '../components/PositionsPanel';
import { OrderBookPanel } from '../components/OrderBookPanel';
import { useTradingStore } from '../stores/trading.store';
import { useWebSocket } from '../hooks/useWebSocket';
import { useMarketData } from '../hooks/useMarketData';
export const TradingPage: React.FC = () => {
const { selectedSymbol } = useTradingStore();
const { subscribe, unsubscribe } = useWebSocket();
const { fetchKlines, fetchTicker } = useMarketData();
useEffect(() => {
// Fetch initial data
fetchKlines(selectedSymbol);
fetchTicker(selectedSymbol);
// Subscribe to real-time updates
subscribe(`kline:${selectedSymbol}:1h`);
subscribe(`ticker:${selectedSymbol}`);
return () => {
unsubscribe(`kline:${selectedSymbol}:1h`);
unsubscribe(`ticker:${selectedSymbol}`);
};
}, [selectedSymbol]);
return (
<div className="h-screen flex flex-col bg-gray-900">
{/* Header */}
<header className="h-16 border-b border-gray-800 flex items-center px-4">
<h1 className="text-xl font-bold text-white">Paper Trading</h1>
</header>
{/* Main content */}
<div className="flex-1 flex overflow-hidden">
{/* Left sidebar - Watchlist */}
<aside className="w-64 border-r border-gray-800 overflow-y-auto">
<WatchlistPanel />
</aside>
{/* Center - Chart */}
<main className="flex-1 flex flex-col">
<div className="flex-1">
<ChartComponent />
</div>
{/* Bottom panels */}
<div className="h-80 border-t border-gray-800 grid grid-cols-3 gap-4 p-4">
<OrderPanel />
<PositionsPanel />
<OrderBookPanel />
</div>
</main>
</div>
</div>
);
};
ChartComponent
Ubicación: apps/frontend/src/modules/trading/components/ChartComponent.tsx
import React, { useEffect, useRef } from 'react';
import { createChart, IChartApi, ISeriesApi } from 'lightweight-charts';
import { useTradingStore } from '../stores/trading.store';
import { useChartStore } from '../stores/chart.store';
export const ChartComponent: React.FC = () => {
const chartContainerRef = useRef<HTMLDivElement>(null);
const chartRef = useRef<IChartApi | null>(null);
const candlestickSeriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null);
const { klines, selectedSymbol, ticker } = useTradingStore();
const { interval, theme, showVolume, showGrid } = useChartStore();
// Initialize chart
useEffect(() => {
if (!chartContainerRef.current) return;
const chart = createChart(chartContainerRef.current, {
width: chartContainerRef.current.clientWidth,
height: chartContainerRef.current.clientHeight,
layout: {
background: { color: theme === 'dark' ? '#1a1a1a' : '#ffffff' },
textColor: theme === 'dark' ? '#d1d5db' : '#1f2937',
},
grid: {
vertLines: { visible: showGrid, color: '#2a2a2a' },
horzLines: { visible: showGrid, color: '#2a2a2a' },
},
crosshair: {
mode: 1,
},
rightPriceScale: {
borderColor: '#2a2a2a',
},
timeScale: {
borderColor: '#2a2a2a',
timeVisible: true,
secondsVisible: false,
},
});
const candlestickSeries = chart.addCandlestickSeries({
upColor: '#22c55e',
downColor: '#ef4444',
borderUpColor: '#22c55e',
borderDownColor: '#ef4444',
wickUpColor: '#22c55e',
wickDownColor: '#ef4444',
});
chartRef.current = chart;
candlestickSeriesRef.current = candlestickSeries;
// Handle resize
const handleResize = () => {
if (chartContainerRef.current) {
chart.applyOptions({
width: chartContainerRef.current.clientWidth,
height: chartContainerRef.current.clientHeight,
});
}
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
chart.remove();
};
}, [theme, showGrid]);
// Update data
useEffect(() => {
if (!candlestickSeriesRef.current || !klines.length) return;
const formattedData = klines.map((k) => ({
time: Math.floor(k.openTime / 1000),
open: parseFloat(k.open),
high: parseFloat(k.high),
low: parseFloat(k.low),
close: parseFloat(k.close),
}));
candlestickSeriesRef.current.setData(formattedData);
}, [klines]);
// Update last price line
useEffect(() => {
if (!chartRef.current || !ticker) return;
chartRef.current.applyOptions({
watermark: {
visible: true,
fontSize: 24,
horzAlign: 'left',
vertAlign: 'top',
color: ticker.priceChange >= 0 ? '#22c55e' : '#ef4444',
text: `${selectedSymbol} ${ticker.price.toFixed(2)}`,
},
});
}, [ticker, selectedSymbol]);
return (
<div className="relative w-full h-full">
{/* Chart toolbar */}
<div className="absolute top-0 left-0 right-0 z-10 bg-gray-800 border-b border-gray-700 px-4 py-2 flex items-center gap-4">
<TimeframeSelector />
<IndicatorSelector />
<ChartTypeSelector />
</div>
{/* Chart container */}
<div ref={chartContainerRef} className="w-full h-full pt-12" />
</div>
);
};
// Sub-components
const TimeframeSelector: React.FC = () => {
const { interval, setInterval } = useChartStore();
const intervals = ['1m', '5m', '15m', '1h', '4h', '1d'] as const;
return (
<div className="flex gap-1">
{intervals.map((int) => (
<button
key={int}
onClick={() => setInterval(int)}
className={`px-3 py-1 text-sm rounded ${
interval === int
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
{int}
</button>
))}
</div>
);
};
const IndicatorSelector: React.FC = () => {
const { indicators, addIndicator } = useChartStore();
const availableIndicators = [
{ type: 'SMA', label: 'SMA(20)' },
{ type: 'EMA', label: 'EMA(20)' },
{ type: 'RSI', label: 'RSI(14)' },
{ type: 'MACD', label: 'MACD' },
{ type: 'BB', label: 'Bollinger Bands' },
];
return (
<select
className="bg-gray-700 text-white px-3 py-1 rounded text-sm"
onChange={(e) => {
if (e.target.value) {
addIndicator({
id: `${e.target.value}-${Date.now()}`,
type: e.target.value as any,
params: {},
visible: true,
});
e.target.value = '';
}
}}
>
<option value="">Add Indicator</option>
{availableIndicators.map((ind) => (
<option key={ind.type} value={ind.type}>
{ind.label}
</option>
))}
</select>
);
};
const ChartTypeSelector: React.FC = () => {
const { chartType, setChartType } = useChartStore();
return (
<select
value={chartType}
onChange={(e) => setChartType(e.target.value as any)}
className="bg-gray-700 text-white px-3 py-1 rounded text-sm"
>
<option value="candlestick">Candlestick</option>
<option value="line">Line</option>
<option value="area">Area</option>
</select>
);
};
OrderPanel
Ubicación: apps/frontend/src/modules/trading/components/OrderPanel.tsx
import React, { useState } from 'react';
import { useOrderStore } from '../stores/order.store';
import { useTradingStore } from '../stores/trading.store';
import { api } from '../services/api';
import { toast } from 'react-hot-toast';
export const OrderPanel: React.FC = () => {
const { selectedSymbol, ticker } = useTradingStore();
const {
orderSide,
setOrderSide,
orderType,
setOrderType,
orderQuantity,
setOrderQuantity,
orderPrice,
setOrderPrice,
stopLoss,
setStopLoss,
takeProfit,
setTakeProfit,
resetOrderForm,
getBalance,
} = useOrderStore();
const [isSubmitting, setIsSubmitting] = useState(false);
const balance = getBalance('USDT');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
try {
const orderData = {
symbol: selectedSymbol,
side: orderSide,
type: orderType,
quantity: parseFloat(orderQuantity),
price: orderType === 'limit' ? parseFloat(orderPrice) : undefined,
stopLoss: stopLoss ? parseFloat(stopLoss) : undefined,
takeProfit: takeProfit ? parseFloat(takeProfit) : undefined,
};
await api.post('/paper/orders', orderData);
toast.success(`${orderSide.toUpperCase()} order placed successfully`);
resetOrderForm();
} catch (error: any) {
toast.error(error.response?.data?.error?.message || 'Failed to place order');
} finally {
setIsSubmitting(false);
}
};
const estimatedCost =
orderType === 'market'
? parseFloat(orderQuantity || '0') * (ticker?.price || 0)
: parseFloat(orderQuantity || '0') * parseFloat(orderPrice || '0');
return (
<div className="bg-gray-800 rounded-lg p-4">
<h3 className="text-lg font-semibold text-white mb-4">Place Order</h3>
{/* Buy/Sell Tabs */}
<div className="flex gap-2 mb-4">
<button
onClick={() => setOrderSide('buy')}
className={`flex-1 py-2 rounded ${
orderSide === 'buy'
? 'bg-green-600 text-white'
: 'bg-gray-700 text-gray-400'
}`}
>
Buy
</button>
<button
onClick={() => setOrderSide('sell')}
className={`flex-1 py-2 rounded ${
orderSide === 'sell'
? 'bg-red-600 text-white'
: 'bg-gray-700 text-gray-400'
}`}
>
Sell
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Order Type */}
<div>
<label className="block text-sm text-gray-400 mb-1">Order Type</label>
<select
value={orderType}
onChange={(e) => setOrderType(e.target.value as any)}
className="w-full bg-gray-700 text-white px-3 py-2 rounded"
>
<option value="market">Market</option>
<option value="limit">Limit</option>
<option value="stop_loss">Stop Loss</option>
</select>
</div>
{/* Price (for limit orders) */}
{orderType === 'limit' && (
<div>
<label className="block text-sm text-gray-400 mb-1">
Price (USDT)
</label>
<input
type="number"
step="0.01"
value={orderPrice}
onChange={(e) => setOrderPrice(e.target.value)}
className="w-full bg-gray-700 text-white px-3 py-2 rounded"
placeholder={ticker?.price.toFixed(2)}
required
/>
</div>
)}
{/* Quantity */}
<div>
<label className="block text-sm text-gray-400 mb-1">Quantity</label>
<input
type="number"
step="0.00000001"
value={orderQuantity}
onChange={(e) => setOrderQuantity(e.target.value)}
className="w-full bg-gray-700 text-white px-3 py-2 rounded"
placeholder="0.00"
required
/>
</div>
{/* Stop Loss */}
<div>
<label className="block text-sm text-gray-400 mb-1">
Stop Loss (Optional)
</label>
<input
type="number"
step="0.01"
value={stopLoss}
onChange={(e) => setStopLoss(e.target.value)}
className="w-full bg-gray-700 text-white px-3 py-2 rounded"
placeholder="0.00"
/>
</div>
{/* Take Profit */}
<div>
<label className="block text-sm text-gray-400 mb-1">
Take Profit (Optional)
</label>
<input
type="number"
step="0.01"
value={takeProfit}
onChange={(e) => setTakeProfit(e.target.value)}
className="w-full bg-gray-700 text-white px-3 py-2 rounded"
placeholder="0.00"
/>
</div>
{/* Summary */}
<div className="bg-gray-700 rounded p-3 space-y-1 text-sm">
<div className="flex justify-between">
<span className="text-gray-400">Available:</span>
<span className="text-white">{balance?.available.toFixed(2)} USDT</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Estimated Cost:</span>
<span className="text-white">{estimatedCost.toFixed(2)} USDT</span>
</div>
</div>
{/* Submit */}
<button
type="submit"
disabled={isSubmitting || !orderQuantity}
className={`w-full py-3 rounded font-semibold ${
orderSide === 'buy'
? 'bg-green-600 hover:bg-green-700'
: 'bg-red-600 hover:bg-red-700'
} text-white disabled:opacity-50 disabled:cursor-not-allowed`}
>
{isSubmitting
? 'Placing Order...'
: `${orderSide === 'buy' ? 'Buy' : 'Sell'} ${selectedSymbol}`}
</button>
</form>
</div>
);
};
PositionsPanel
Ubicación: apps/frontend/src/modules/trading/components/PositionsPanel.tsx
import React, { useEffect } from 'react';
import { useOrderStore } from '../stores/order.store';
import { api } from '../services/api';
import { toast } from 'react-hot-toast';
export const PositionsPanel: React.FC = () => {
const { positions, setPositions } = useOrderStore();
useEffect(() => {
fetchPositions();
}, []);
const fetchPositions = async () => {
try {
const { data } = await api.get('/paper/positions', {
params: { status: 'open' },
});
setPositions(data.data);
} catch (error) {
console.error('Failed to fetch positions:', error);
}
};
const handleClosePosition = async (positionId: string) => {
try {
await api.post(`/paper/positions/${positionId}/close`);
toast.success('Position closed');
fetchPositions();
} catch (error: any) {
toast.error('Failed to close position');
}
};
return (
<div className="bg-gray-800 rounded-lg p-4">
<h3 className="text-lg font-semibold text-white mb-4">Open Positions</h3>
{positions.length === 0 ? (
<p className="text-gray-400 text-sm text-center py-8">
No open positions
</p>
) : (
<div className="space-y-2">
{positions.map((position) => (
<div
key={position.id}
className="bg-gray-700 rounded p-3 space-y-2"
>
<div className="flex justify-between items-start">
<div>
<div className="font-semibold text-white">
{position.symbol}
</div>
<div className="text-xs text-gray-400">
{position.side.toUpperCase()} {position.currentQuantity}
</div>
</div>
<div
className={`font-semibold ${
position.totalPnl >= 0 ? 'text-green-500' : 'text-red-500'
}`}
>
{position.totalPnl >= 0 ? '+' : ''}
{position.totalPnl.toFixed(2)} USDT
<div className="text-xs">
({position.pnlPercentage.toFixed(2)}%)
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div>
<span className="text-gray-400">Entry:</span>{' '}
<span className="text-white">
{position.averageEntryPrice.toFixed(2)}
</span>
</div>
<div>
<span className="text-gray-400">Value:</span>{' '}
<span className="text-white">
{(
position.currentQuantity * position.averageEntryPrice
).toFixed(2)}
</span>
</div>
</div>
<button
onClick={() => handleClosePosition(position.id)}
className="w-full bg-red-600 hover:bg-red-700 text-white text-sm py-1 rounded"
>
Close Position
</button>
</div>
))}
</div>
)}
</div>
);
};
Hooks Personalizados
useMarketData
// hooks/useMarketData.ts
import { useTradingStore } from '../stores/trading.store';
import { api } from '../services/api';
export function useMarketData() {
const { setKlines, setTicker, setOrderBook } = useTradingStore();
const fetchKlines = async (symbol: string, interval: string = '1h') => {
try {
const { data } = await api.get('/market/klines', {
params: { symbol, interval, limit: 500 },
});
setKlines(data.data);
} catch (error) {
console.error('Failed to fetch klines:', error);
}
};
const fetchTicker = async (symbol: string) => {
try {
const { data } = await api.get(`/market/ticker/${symbol}`);
setTicker(data.data);
} catch (error) {
console.error('Failed to fetch ticker:', error);
}
};
const fetchOrderBook = async (symbol: string) => {
try {
const { data } = await api.get(`/market/orderbook/${symbol}`);
setOrderBook(data.data);
} catch (error) {
console.error('Failed to fetch order book:', error);
}
};
return { fetchKlines, fetchTicker, fetchOrderBook };
}
Dependencias
{
"dependencies": {
"react": "^18.2.0",
"zustand": "^4.4.7",
"lightweight-charts": "^4.1.0",
"axios": "^1.6.0",
"react-hot-toast": "^2.4.1",
"tailwindcss": "^3.4.0"
}
}