trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/especificaciones/ET-TRD-005-frontend.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

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"
  }
}

Referencias