# ET-TRD-010: Drawing Tools Persistence

**Versión:** 1.0.0
**Fecha:** 2026-01-25
**Epic:** OQI-003 - Trading y Charts
**Componente:** Backend API + Frontend Store + Database Schema
**Estado:** 🟡 50% Implementado (UI funciona, falta persistencia)
**Prioridad:** P2

---

## Metadata

| Campo | Valor |
|-------|-------|
| **ID** | ET-TRD-010 |
| **Tipo** | Especificación Técnica |
| **Epic** | OQI-003 |
| **US Relacionada** | US-TRD-010 (Persistir Dibujos en Charts) |
| **Componente Actual** | `ChartDrawingToolsPanel.tsx` (420 líneas, ✅ funcional) |
| **Gap** | Persistencia (backend API + DB + Zustand store) |
| **Complejidad** | Media (CRUD + sincronización) |
| **Esfuerzo Estimado** | 3 horas |

---

## 1. Descripción General

**Drawing Tools Persistence** permite a los usuarios guardar sus análisis técnicos (líneas de tendencia, Fibonacci, rectángulos, anotaciones, etc.) en el backend para que persistan entre sesiones y dispositivos.

### Estado Actual (50% Implementado)

✅ **Frontend UI Completado:**
- Componente `ChartDrawingToolsPanel.tsx` con 10 herramientas de dibujo
- Gestión de estado local (color, grosor, visibilidad, bloqueo)
- Duplicación, eliminación, undo
- 420 líneas de código funcional

❌ **Falta Implementar:**
- Backend API REST (`/api/chart-drawings`)
- Tabla `chart_drawings` en PostgreSQL
- Zustand store para gestión de estado
- Hook `useDrawings` para sincronización
- Auto-save debounced (1 segundo)
- Import/export JSON

---

## 2. Arquitectura de Persistencia

### 2.1 Flujo de Datos

```
┌─────────────────────────────────────────────────────────────┐
│              Drawing Persistence Architecture                │
├─────────────────────────────────────────────────────────────┤
│                                                               │
│  User Interaction (Chart Canvas)                             │
│         │                                                     │
│         v                                                     │
│  ┌──────────────────────────────────────┐                   │
│  │  ChartDrawingToolsPanel.tsx          │                   │
│  │  • Draw, Edit, Delete                │                   │
│  │  • Local state management            │                   │
│  └──────────────┬───────────────────────┘                   │
│                 │                                             │
│                 v                                             │
│  ┌──────────────────────────────────────┐                   │
│  │  useDrawings Hook                    │                   │
│  │  • Debounce changes (1s)             │                   │
│  │  • Call Zustand actions              │                   │
│  └──────────────┬───────────────────────┘                   │
│                 │                                             │
│                 v                                             │
│  ┌──────────────────────────────────────┐                   │
│  │  drawingsStore (Zustand)             │                   │
│  │  • Local cache                        │                   │
│  │  • Optimistic updates                 │                   │
│  │  • API calls                          │                   │
│  └──────────────┬───────────────────────┘                   │
│                 │                                             │
│         ┌───────┴────────┐                                   │
│         │                │                                    │
│         v                v                                    │
│  POST /api/drawings  GET /api/drawings/:symbol               │
│         │                │                                    │
│         v                v                                    │
│  ┌──────────────────────────────────────┐                   │
│  │  Express Backend                     │                   │
│  │  • ChartDrawingsController           │                   │
│  │  • Validation                         │                   │
│  │  • Auth check (JWT)                   │                   │
│  └──────────────┬───────────────────────┘                   │
│                 │                                             │
│                 v                                             │
│  ┌──────────────────────────────────────┐                   │
│  │  PostgreSQL: chart_drawings          │                   │
│  │  • user_id, symbol, timeframe        │                   │
│  │  • drawing_data (JSONB)               │                   │
│  │  • created_at, updated_at             │                   │
│  └──────────────────────────────────────┘                   │
│                                                               │
└─────────────────────────────────────────────────────────────┘
```

---

## 3. Database Schema

### 3.1 Tabla `chart_drawings`

```sql
-- Schema: trading
CREATE TABLE IF NOT EXISTS trading.chart_drawings (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES core.users(id) ON DELETE CASCADE,

  -- Chart context
  symbol VARCHAR(20) NOT NULL,  -- BTCUSD, ETHUSD, etc.
  timeframe VARCHAR(10) NOT NULL,  -- 1m, 5m, 15m, 1h, 4h, 1d

  -- Drawing metadata
  tool_type VARCHAR(20) NOT NULL,  -- trendline, fibonacci, rectangle, etc.
  label VARCHAR(100),  -- Optional user label

  -- Drawing properties
  drawing_data JSONB NOT NULL,  -- Complete drawing object (color, points, etc.)

  -- State
  visible BOOLEAN DEFAULT true,
  locked BOOLEAN DEFAULT false,

  -- Timestamps
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

  -- Indexes
  CONSTRAINT unique_user_symbol_drawing UNIQUE (user_id, symbol, id)
);

-- Indexes for performance
CREATE INDEX idx_chart_drawings_user_symbol ON trading.chart_drawings(user_id, symbol);
CREATE INDEX idx_chart_drawings_user_symbol_timeframe ON trading.chart_drawings(user_id, symbol, timeframe);
CREATE INDEX idx_chart_drawings_created_at ON trading.chart_drawings(created_at DESC);

-- GIN index for JSONB queries
CREATE INDEX idx_chart_drawings_data ON trading.chart_drawings USING GIN(drawing_data);

-- Auto-update timestamp
CREATE TRIGGER update_chart_drawings_updated_at
  BEFORE UPDATE ON trading.chart_drawings
  FOR EACH ROW
  EXECUTE FUNCTION core.update_updated_at_column();
```

### 3.2 Estructura de `drawing_data` (JSONB)

```json
{
  "id": "draw_1738045812345_abc123",
  "tool": "trendline",
  "color": "#3B82F6",
  "lineWidth": 2,
  "visible": true,
  "locked": false,
  "label": "Strong uptrend",
  "points": [
    { "x": 100, "y": 200, "price": 43250.50, "time": "2026-01-25T10:00:00Z" },
    { "x": 500, "y": 150, "price": 44100.00, "time": "2026-01-25T14:00:00Z" }
  ],
  "createdAt": "2026-01-25T10:30:15Z"
}
```

---

## 4. Backend API

### 4.1 REST Endpoints

| Método | Endpoint | Descripción | Auth |
|--------|----------|-------------|------|
| GET | `/api/chart-drawings/:symbol` | Obtener todos los dibujos de un símbolo | ✅ JWT |
| GET | `/api/chart-drawings/:symbol/:timeframe` | Filtrar por timeframe | ✅ JWT |
| POST | `/api/chart-drawings` | Crear nuevo dibujo | ✅ JWT |
| PUT | `/api/chart-drawings/:id` | Actualizar dibujo existente | ✅ JWT |
| DELETE | `/api/chart-drawings/:id` | Eliminar dibujo | ✅ JWT |
| DELETE | `/api/chart-drawings/:symbol/all` | Eliminar todos los dibujos de un símbolo | ✅ JWT |
| POST | `/api/chart-drawings/import` | Importar dibujos desde JSON | ✅ JWT |
| GET | `/api/chart-drawings/export/:symbol` | Exportar dibujos a JSON | ✅ JWT |

### 4.2 Request/Response Examples

#### POST `/api/chart-drawings` - Create Drawing

**Request:**
```http
POST /api/chart-drawings
Authorization: Bearer <jwt_token>
Content-Type: application/json

{
  "symbol": "BTCUSD",
  "timeframe": "1h",
  "tool_type": "trendline",
  "label": "Strong uptrend",
  "drawing_data": {
    "id": "draw_1738045812345_abc123",
    "tool": "trendline",
    "color": "#3B82F6",
    "lineWidth": 2,
    "visible": true,
    "locked": false,
    "points": [
      { "x": 100, "y": 200, "price": 43250.50, "time": "2026-01-25T10:00:00Z" },
      { "x": 500, "y": 150, "price": 44100.00, "time": "2026-01-25T14:00:00Z" }
    ],
    "createdAt": "2026-01-25T10:30:15Z"
  },
  "visible": true,
  "locked": false
}
```

**Response:**
```json
{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "user_id": "123e4567-e89b-12d3-a456-426614174000",
    "symbol": "BTCUSD",
    "timeframe": "1h",
    "tool_type": "trendline",
    "label": "Strong uptrend",
    "drawing_data": { /* ... */ },
    "visible": true,
    "locked": false,
    "created_at": "2026-01-25T10:30:15Z",
    "updated_at": "2026-01-25T10:30:15Z"
  }
}
```

#### GET `/api/chart-drawings/:symbol` - Fetch Drawings

**Request:**
```http
GET /api/chart-drawings/BTCUSD?timeframe=1h
Authorization: Bearer <jwt_token>
```

**Response:**
```json
{
  "success": true,
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "symbol": "BTCUSD",
      "timeframe": "1h",
      "tool_type": "trendline",
      "label": "Strong uptrend",
      "drawing_data": {
        "id": "draw_1738045812345_abc123",
        "tool": "trendline",
        "color": "#3B82F6",
        "lineWidth": 2,
        "visible": true,
        "locked": false,
        "points": [
          { "x": 100, "y": 200, "price": 43250.50, "time": "2026-01-25T10:00:00Z" },
          { "x": 500, "y": 150, "price": 44100.00, "time": "2026-01-25T14:00:00Z" }
        ],
        "createdAt": "2026-01-25T10:30:15Z"
      },
      "visible": true,
      "locked": false,
      "created_at": "2026-01-25T10:30:15Z",
      "updated_at": "2026-01-25T10:30:15Z"
    },
    {
      "id": "660f9511-f39c-52e5-b827-557766551111",
      "symbol": "BTCUSD",
      "timeframe": "1h",
      "tool_type": "fibonacci",
      "label": "Fib retracement",
      "drawing_data": { /* ... */ },
      "visible": true,
      "locked": false,
      "created_at": "2026-01-25T11:00:00Z",
      "updated_at": "2026-01-25T11:00:00Z"
    }
  ],
  "meta": {
    "total": 2,
    "symbol": "BTCUSD",
    "timeframe": "1h"
  }
}
```

#### PUT `/api/chart-drawings/:id` - Update Drawing

**Request:**
```http
PUT /api/chart-drawings/550e8400-e29b-41d4-a716-446655440000
Authorization: Bearer <jwt_token>
Content-Type: application/json

{
  "drawing_data": {
    "id": "draw_1738045812345_abc123",
    "tool": "trendline",
    "color": "#10B981",  // Changed color
    "lineWidth": 3,      // Changed width
    "visible": true,
    "locked": true,      // Locked
    "points": [
      { "x": 100, "y": 200, "price": 43250.50, "time": "2026-01-25T10:00:00Z" },
      { "x": 500, "y": 150, "price": 44100.00, "time": "2026-01-25T14:00:00Z" }
    ],
    "createdAt": "2026-01-25T10:30:15Z"
  },
  "locked": true
}
```

**Response:**
```json
{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "drawing_data": { /* updated data */ },
    "locked": true,
    "updated_at": "2026-01-25T11:15:30Z"
  }
}
```

---

## 5. Backend Implementation

### 5.1 Controller: `ChartDrawingsController.ts`

```typescript
// apps/backend/src/controllers/chart-drawings.controller.ts

import { Request, Response } from 'express';
import { pool } from '../config/database';
import { authenticateToken } from '../middleware/auth.middleware';

interface Drawing {
  id: string;
  tool: string;
  color: string;
  lineWidth: number;
  visible: boolean;
  locked: boolean;
  label?: string;
  points: { x: number; y: number; price?: number; time?: string }[];
  createdAt: string;
}

interface CreateDrawingRequest {
  symbol: string;
  timeframe: string;
  tool_type: string;
  label?: string;
  drawing_data: Drawing;
  visible?: boolean;
  locked?: boolean;
}

export class ChartDrawingsController {

  // GET /api/chart-drawings/:symbol
  static async getDrawings(req: Request, res: Response) {
    const { symbol } = req.params;
    const { timeframe } = req.query;
    const userId = req.user!.id;

    try {
      let query = `
        SELECT * FROM trading.chart_drawings
        WHERE user_id = $1 AND symbol = $2
      `;
      const params: any[] = [userId, symbol];

      if (timeframe) {
        query += ` AND timeframe = $3`;
        params.push(timeframe);
      }

      query += ` ORDER BY created_at DESC`;

      const result = await pool.query(query, params);

      res.json({
        success: true,
        data: result.rows,
        meta: {
          total: result.rows.length,
          symbol,
          timeframe: timeframe || 'all'
        }
      });
    } catch (error) {
      console.error('Error fetching drawings:', error);
      res.status(500).json({
        success: false,
        error: 'Failed to fetch drawings'
      });
    }
  }

  // POST /api/chart-drawings
  static async createDrawing(req: Request, res: Response) {
    const userId = req.user!.id;
    const { symbol, timeframe, tool_type, label, drawing_data, visible = true, locked = false } = req.body as CreateDrawingRequest;

    // Validation
    if (!symbol || !timeframe || !tool_type || !drawing_data) {
      return res.status(400).json({
        success: false,
        error: 'Missing required fields: symbol, timeframe, tool_type, drawing_data'
      });
    }

    try {
      const query = `
        INSERT INTO trading.chart_drawings (
          user_id, symbol, timeframe, tool_type, label, drawing_data, visible, locked
        ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
        RETURNING *
      `;

      const result = await pool.query(query, [
        userId,
        symbol,
        timeframe,
        tool_type,
        label,
        JSON.stringify(drawing_data),
        visible,
        locked
      ]);

      res.status(201).json({
        success: true,
        data: result.rows[0]
      });
    } catch (error) {
      console.error('Error creating drawing:', error);
      res.status(500).json({
        success: false,
        error: 'Failed to create drawing'
      });
    }
  }

  // PUT /api/chart-drawings/:id
  static async updateDrawing(req: Request, res: Response) {
    const { id } = req.params;
    const userId = req.user!.id;
    const { drawing_data, visible, locked, label } = req.body;

    try {
      // Build dynamic update query
      const updates: string[] = [];
      const values: any[] = [];
      let paramCount = 1;

      if (drawing_data !== undefined) {
        updates.push(`drawing_data = $${paramCount++}`);
        values.push(JSON.stringify(drawing_data));
      }
      if (visible !== undefined) {
        updates.push(`visible = $${paramCount++}`);
        values.push(visible);
      }
      if (locked !== undefined) {
        updates.push(`locked = $${paramCount++}`);
        values.push(locked);
      }
      if (label !== undefined) {
        updates.push(`label = $${paramCount++}`);
        values.push(label);
      }

      if (updates.length === 0) {
        return res.status(400).json({
          success: false,
          error: 'No fields to update'
        });
      }

      const query = `
        UPDATE trading.chart_drawings
        SET ${updates.join(', ')}, updated_at = CURRENT_TIMESTAMP
        WHERE id = $${paramCount} AND user_id = $${paramCount + 1}
        RETURNING *
      `;

      values.push(id, userId);

      const result = await pool.query(query, values);

      if (result.rows.length === 0) {
        return res.status(404).json({
          success: false,
          error: 'Drawing not found or unauthorized'
        });
      }

      res.json({
        success: true,
        data: result.rows[0]
      });
    } catch (error) {
      console.error('Error updating drawing:', error);
      res.status(500).json({
        success: false,
        error: 'Failed to update drawing'
      });
    }
  }

  // DELETE /api/chart-drawings/:id
  static async deleteDrawing(req: Request, res: Response) {
    const { id } = req.params;
    const userId = req.user!.id;

    try {
      const query = `
        DELETE FROM trading.chart_drawings
        WHERE id = $1 AND user_id = $2
        RETURNING id
      `;

      const result = await pool.query(query, [id, userId]);

      if (result.rows.length === 0) {
        return res.status(404).json({
          success: false,
          error: 'Drawing not found or unauthorized'
        });
      }

      res.json({
        success: true,
        message: 'Drawing deleted successfully'
      });
    } catch (error) {
      console.error('Error deleting drawing:', error);
      res.status(500).json({
        success: false,
        error: 'Failed to delete drawing'
      });
    }
  }

  // DELETE /api/chart-drawings/:symbol/all
  static async clearAllDrawings(req: Request, res: Response) {
    const { symbol } = req.params;
    const userId = req.user!.id;
    const { timeframe } = req.query;

    try {
      let query = `
        DELETE FROM trading.chart_drawings
        WHERE user_id = $1 AND symbol = $2
      `;
      const params: any[] = [userId, symbol];

      if (timeframe) {
        query += ` AND timeframe = $3`;
        params.push(timeframe);
      }

      query += ` RETURNING id`;

      const result = await pool.query(query, params);

      res.json({
        success: true,
        message: `Deleted ${result.rows.length} drawings`,
        count: result.rows.length
      });
    } catch (error) {
      console.error('Error clearing drawings:', error);
      res.status(500).json({
        success: false,
        error: 'Failed to clear drawings'
      });
    }
  }

  // POST /api/chart-drawings/import
  static async importDrawings(req: Request, res: Response) {
    const userId = req.user!.id;
    const { drawings } = req.body;

    if (!Array.isArray(drawings) || drawings.length === 0) {
      return res.status(400).json({
        success: false,
        error: 'Invalid drawings array'
      });
    }

    try {
      const insertedDrawings = [];

      for (const drawing of drawings) {
        const { symbol, timeframe, tool_type, label, drawing_data, visible, locked } = drawing;

        const query = `
          INSERT INTO trading.chart_drawings (
            user_id, symbol, timeframe, tool_type, label, drawing_data, visible, locked
          ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
          RETURNING *
        `;

        const result = await pool.query(query, [
          userId,
          symbol,
          timeframe,
          tool_type,
          label,
          JSON.stringify(drawing_data),
          visible ?? true,
          locked ?? false
        ]);

        insertedDrawings.push(result.rows[0]);
      }

      res.json({
        success: true,
        message: `Imported ${insertedDrawings.length} drawings`,
        data: insertedDrawings
      });
    } catch (error) {
      console.error('Error importing drawings:', error);
      res.status(500).json({
        success: false,
        error: 'Failed to import drawings'
      });
    }
  }

  // GET /api/chart-drawings/export/:symbol
  static async exportDrawings(req: Request, res: Response) {
    const { symbol } = req.params;
    const userId = req.user!.id;
    const { timeframe } = req.query;

    try {
      let query = `
        SELECT * FROM trading.chart_drawings
        WHERE user_id = $1 AND symbol = $2
      `;
      const params: any[] = [userId, symbol];

      if (timeframe) {
        query += ` AND timeframe = $3`;
        params.push(timeframe);
      }

      const result = await pool.query(query, params);

      const exportData = {
        version: '1.0.0',
        exported_at: new Date().toISOString(),
        symbol,
        timeframe: timeframe || 'all',
        count: result.rows.length,
        drawings: result.rows
      };

      res.json({
        success: true,
        data: exportData
      });
    } catch (error) {
      console.error('Error exporting drawings:', error);
      res.status(500).json({
        success: false,
        error: 'Failed to export drawings'
      });
    }
  }
}
```

### 5.2 Routes: `chart-drawings.routes.ts`

```typescript
// apps/backend/src/routes/chart-drawings.routes.ts

import { Router } from 'express';
import { ChartDrawingsController } from '../controllers/chart-drawings.controller';
import { authenticateToken } from '../middleware/auth.middleware';

const router = Router();

// All routes require authentication
router.use(authenticateToken);

// CRUD operations
router.get('/chart-drawings/:symbol', ChartDrawingsController.getDrawings);
router.post('/chart-drawings', ChartDrawingsController.createDrawing);
router.put('/chart-drawings/:id', ChartDrawingsController.updateDrawing);
router.delete('/chart-drawings/:id', ChartDrawingsController.deleteDrawing);
router.delete('/chart-drawings/:symbol/all', ChartDrawingsController.clearAllDrawings);

// Import/Export
router.post('/chart-drawings/import', ChartDrawingsController.importDrawings);
router.get('/chart-drawings/export/:symbol', ChartDrawingsController.exportDrawings);

export default router;
```

---

## 6. Frontend Implementation

### 6.1 Zustand Store: `drawingsStore.ts`

```typescript
// apps/frontend/src/stores/drawingsStore.ts

import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { apiClient } from '../lib/apiClient';

export interface Drawing {
  id: string;
  tool: string;
  color: string;
  lineWidth: number;
  visible: boolean;
  locked: boolean;
  label?: string;
  points: { x: number; y: number; price?: number; time?: string }[];
  createdAt: string;
}

interface ServerDrawing {
  id: string;
  user_id: string;
  symbol: string;
  timeframe: string;
  tool_type: string;
  label?: string;
  drawing_data: Drawing;
  visible: boolean;
  locked: boolean;
  created_at: string;
  updated_at: string;
}

interface DrawingsState {
  // State
  drawings: Map<string, Drawing[]>; // key: "symbol:timeframe"
  loading: boolean;
  error: string | null;

  // Actions
  fetchDrawings: (symbol: string, timeframe: string) => Promise<void>;
  addDrawing: (symbol: string, timeframe: string, drawing: Drawing) => Promise<void>;
  updateDrawing: (symbol: string, timeframe: string, drawing: Drawing) => Promise<void>;
  removeDrawing: (symbol: string, timeframe: string, drawingId: string) => Promise<void>;
  clearAllDrawings: (symbol: string, timeframe: string) => Promise<void>;
  importDrawings: (drawings: any[]) => Promise<void>;
  exportDrawings: (symbol: string, timeframe?: string) => Promise<any>;

  // Local actions (optimistic updates)
  addDrawingLocally: (symbol: string, timeframe: string, drawing: Drawing) => void;
  updateDrawingLocally: (symbol: string, timeframe: string, drawing: Drawing) => void;
  removeDrawingLocally: (symbol: string, timeframe: string, drawingId: string) => void;
}

export const useDrawingsStore = create<DrawingsState>()(
  devtools(
    (set, get) => ({
      drawings: new Map(),
      loading: false,
      error: null,

      // Fetch all drawings for a symbol+timeframe
      fetchDrawings: async (symbol: string, timeframe: string) => {
        set({ loading: true, error: null });

        try {
          const response = await apiClient.get(`/api/chart-drawings/${symbol}`, {
            params: { timeframe }
          });

          const serverDrawings: ServerDrawing[] = response.data.data;
          const drawings = serverDrawings.map(d => d.drawing_data);

          const key = `${symbol}:${timeframe}`;
          const newMap = new Map(get().drawings);
          newMap.set(key, drawings);

          set({ drawings: newMap, loading: false });
        } catch (error) {
          console.error('Error fetching drawings:', error);
          set({
            error: error instanceof Error ? error.message : 'Failed to fetch drawings',
            loading: false
          });
        }
      },

      // Add new drawing (optimistic update + API call)
      addDrawing: async (symbol: string, timeframe: string, drawing: Drawing) => {
        // Optimistic update
        get().addDrawingLocally(symbol, timeframe, drawing);

        try {
          await apiClient.post('/api/chart-drawings', {
            symbol,
            timeframe,
            tool_type: drawing.tool,
            label: drawing.label,
            drawing_data: drawing,
            visible: drawing.visible,
            locked: drawing.locked
          });
        } catch (error) {
          console.error('Error saving drawing:', error);
          // Rollback optimistic update
          get().removeDrawingLocally(symbol, timeframe, drawing.id);
          set({
            error: error instanceof Error ? error.message : 'Failed to save drawing'
          });
          throw error;
        }
      },

      // Update existing drawing
      updateDrawing: async (symbol: string, timeframe: string, drawing: Drawing) => {
        // Find server ID (we need to maintain a mapping or fetch it)
        // For simplicity, we'll refetch after update

        try {
          // This requires knowing the server UUID
          // In practice, we'd maintain a clientId -> serverId mapping
          // For now, we'll use drawing.id as both (needs adjustment)

          await apiClient.put(`/api/chart-drawings/${drawing.id}`, {
            drawing_data: drawing,
            visible: drawing.visible,
            locked: drawing.locked,
            label: drawing.label
          });

          // Optimistic update
          get().updateDrawingLocally(symbol, timeframe, drawing);
        } catch (error) {
          console.error('Error updating drawing:', error);
          set({
            error: error instanceof Error ? error.message : 'Failed to update drawing'
          });
          throw error;
        }
      },

      // Remove drawing
      removeDrawing: async (symbol: string, timeframe: string, drawingId: string) => {
        // Optimistic update
        const key = `${symbol}:${timeframe}`;
        const currentDrawings = get().drawings.get(key) || [];
        const drawingToRemove = currentDrawings.find(d => d.id === drawingId);

        get().removeDrawingLocally(symbol, timeframe, drawingId);

        try {
          await apiClient.delete(`/api/chart-drawings/${drawingId}`);
        } catch (error) {
          console.error('Error deleting drawing:', error);
          // Rollback
          if (drawingToRemove) {
            get().addDrawingLocally(symbol, timeframe, drawingToRemove);
          }
          set({
            error: error instanceof Error ? error.message : 'Failed to delete drawing'
          });
          throw error;
        }
      },

      // Clear all drawings
      clearAllDrawings: async (symbol: string, timeframe: string) => {
        const key = `${symbol}:${timeframe}`;
        const backup = get().drawings.get(key) || [];

        // Optimistic clear
        const newMap = new Map(get().drawings);
        newMap.set(key, []);
        set({ drawings: newMap });

        try {
          await apiClient.delete(`/api/chart-drawings/${symbol}/all`, {
            params: { timeframe }
          });
        } catch (error) {
          console.error('Error clearing drawings:', error);
          // Rollback
          const rollbackMap = new Map(get().drawings);
          rollbackMap.set(key, backup);
          set({
            drawings: rollbackMap,
            error: error instanceof Error ? error.message : 'Failed to clear drawings'
          });
          throw error;
        }
      },

      // Import drawings
      importDrawings: async (drawings: any[]) => {
        try {
          await apiClient.post('/api/chart-drawings/import', { drawings });
          // Refetch current symbol/timeframe after import
          // (caller should trigger refetch)
        } catch (error) {
          console.error('Error importing drawings:', error);
          set({
            error: error instanceof Error ? error.message : 'Failed to import drawings'
          });
          throw error;
        }
      },

      // Export drawings
      exportDrawings: async (symbol: string, timeframe?: string) => {
        try {
          const response = await apiClient.get(`/api/chart-drawings/export/${symbol}`, {
            params: { timeframe }
          });
          return response.data.data;
        } catch (error) {
          console.error('Error exporting drawings:', error);
          set({
            error: error instanceof Error ? error.message : 'Failed to export drawings'
          });
          throw error;
        }
      },

      // Local optimistic updates
      addDrawingLocally: (symbol: string, timeframe: string, drawing: Drawing) => {
        const key = `${symbol}:${timeframe}`;
        const current = get().drawings.get(key) || [];
        const newMap = new Map(get().drawings);
        newMap.set(key, [...current, drawing]);
        set({ drawings: newMap });
      },

      updateDrawingLocally: (symbol: string, timeframe: string, drawing: Drawing) => {
        const key = `${symbol}:${timeframe}`;
        const current = get().drawings.get(key) || [];
        const updated = current.map(d => d.id === drawing.id ? drawing : d);
        const newMap = new Map(get().drawings);
        newMap.set(key, updated);
        set({ drawings: newMap });
      },

      removeDrawingLocally: (symbol: string, timeframe: string, drawingId: string) => {
        const key = `${symbol}:${timeframe}`;
        const current = get().drawings.get(key) || [];
        const filtered = current.filter(d => d.id !== drawingId);
        const newMap = new Map(get().drawings);
        newMap.set(key, filtered);
        set({ drawings: newMap });
      },
    }),
    { name: 'drawings-store' }
  )
);
```

### 6.2 Hook: `useDrawings.ts`

```typescript
// apps/frontend/src/modules/trading/hooks/useDrawings.ts

import { useEffect, useRef, useCallback } from 'react';
import { useDrawingsStore } from '../../../stores/drawingsStore';
import { useDebouncedCallback } from 'use-debounce';
import type { Drawing } from '../../../stores/drawingsStore';

export const useDrawings = (symbol: string, timeframe: string) => {
  const {
    drawings,
    loading,
    error,
    fetchDrawings,
    addDrawing,
    updateDrawing,
    removeDrawing,
    clearAllDrawings,
  } = useDrawingsStore();

  const key = `${symbol}:${timeframe}`;
  const currentDrawings = drawings.get(key) || [];

  // Fetch on mount and when symbol/timeframe changes
  useEffect(() => {
    fetchDrawings(symbol, timeframe);
  }, [symbol, timeframe]);

  // Debounced save (1 second delay)
  const debouncedUpdate = useDebouncedCallback(
    (drawing: Drawing) => {
      updateDrawing(symbol, timeframe, drawing);
    },
    1000
  );

  // API methods
  const handleAddDrawing = useCallback(async (drawing: Drawing) => {
    await addDrawing(symbol, timeframe, drawing);
  }, [symbol, timeframe, addDrawing]);

  const handleUpdateDrawing = useCallback((drawing: Drawing, immediate = false) => {
    if (immediate) {
      updateDrawing(symbol, timeframe, drawing);
    } else {
      debouncedUpdate(drawing);
    }
  }, [symbol, timeframe, updateDrawing, debouncedUpdate]);

  const handleRemoveDrawing = useCallback(async (drawingId: string) => {
    await removeDrawing(symbol, timeframe, drawingId);
  }, [symbol, timeframe, removeDrawing]);

  const handleClearAll = useCallback(async () => {
    await clearAllDrawings(symbol, timeframe);
  }, [symbol, timeframe, clearAllDrawings]);

  return {
    drawings: currentDrawings,
    loading,
    error,
    addDrawing: handleAddDrawing,
    updateDrawing: handleUpdateDrawing,
    removeDrawing: handleRemoveDrawing,
    clearAll: handleClearAll,
    refetch: () => fetchDrawings(symbol, timeframe),
  };
};
```

### 6.3 Integration with `ChartDrawingToolsPanel.tsx`

```tsx
// apps/frontend/src/modules/trading/components/ChartDrawingToolsPanel.tsx
// MODIFICACIÓN: Integrar con useDrawings hook

import { useDrawings } from '../hooks/useDrawings';
import { useTradingStore } from '../../../stores/tradingStore';

const ChartDrawingToolsPanelWithPersistence: React.FC = () => {
  const { selectedSymbol, timeframe } = useTradingStore();
  const {
    drawings,
    loading,
    error,
    addDrawing,
    updateDrawing,
    removeDrawing,
    clearAll
  } = useDrawings(selectedSymbol, timeframe);

  const [activeTool, setActiveTool] = useState<DrawingTool | null>(null);

  const handleDrawingAdd = async (drawing: Drawing) => {
    try {
      await addDrawing(drawing);
    } catch (error) {
      console.error('Failed to save drawing:', error);
      // Show toast notification
    }
  };

  const handleDrawingUpdate = (drawing: Drawing) => {
    // Debounced auto-save (1 second)
    updateDrawing(drawing, false);
  };

  const handleDrawingRemove = async (drawingId: string) => {
    try {
      await removeDrawing(drawingId);
    } catch (error) {
      console.error('Failed to delete drawing:', error);
    }
  };

  const handleClearAll = async () => {
    if (confirm(`Delete all ${drawings.length} drawings?`)) {
      try {
        await clearAll();
      } catch (error) {
        console.error('Failed to clear drawings:', error);
      }
    }
  };

  return (
    <ChartDrawingToolsPanel
      drawings={drawings}
      activeTool={activeTool}
      onToolSelect={setActiveTool}
      onDrawingAdd={handleDrawingAdd}
      onDrawingUpdate={handleDrawingUpdate}
      onDrawingRemove={handleDrawingRemove}
      onClearAll={handleClearAll}
    />
  );
};
```

---

## 7. Tipos TypeScript

### 7.1 Frontend Types

```typescript
// apps/frontend/src/types/drawings.types.ts

export type DrawingTool =
  | 'trendline'
  | 'horizontal'
  | 'vertical'
  | 'rectangle'
  | 'circle'
  | 'fibonacci'
  | 'text'
  | 'arrow'
  | 'channel'
  | 'pitchfork';

export interface DrawingPoint {
  x: number;
  y: number;
  price?: number;
  time?: string;
}

export interface Drawing {
  id: string;
  tool: DrawingTool;
  color: string;
  lineWidth: number;
  visible: boolean;
  locked: boolean;
  label?: string;
  points: DrawingPoint[];
  createdAt: string;
}

export interface ServerDrawing {
  id: string;
  user_id: string;
  symbol: string;
  timeframe: string;
  tool_type: DrawingTool;
  label?: string;
  drawing_data: Drawing;
  visible: boolean;
  locked: boolean;
  created_at: string;
  updated_at: string;
}
```

---

## 8. Performance Optimizations

### 8.1 Auto-Save Strategy

- **Debounced Save:** 1 segundo de delay después del último cambio
- **Immediate Save:** Eventos críticos (delete, lock, visibility toggle)
- **Optimistic Updates:** UI actualiza inmediatamente, rollback en error

### 8.2 Caching

- Zustand store mantiene caché por `symbol:timeframe`
- Re-fetch solo cuando cambia symbol/timeframe
- LocalStorage backup (opcional, para offline mode)

### 8.3 Database Indexes

- Index compuesto `(user_id, symbol, timeframe)` para queries rápidas
- GIN index en `drawing_data` JSONB para búsquedas por propiedades

---

## 9. Testing Checklist

### Backend Tests
- [ ] Create drawing with valid data
- [ ] Create drawing with missing fields (validation)
- [ ] Fetch drawings by symbol
- [ ] Fetch drawings by symbol + timeframe
- [ ] Update drawing (color, position, lock)
- [ ] Delete drawing
- [ ] Delete all drawings for symbol
- [ ] Unauthorized access (different user)
- [ ] Import/export JSON
- [ ] JSONB query performance (>1000 drawings)

### Frontend Tests
- [ ] Draw line, save, reload → persists
- [ ] Update color → auto-saves after 1 second
- [ ] Delete drawing → removed from DB
- [ ] Lock drawing → prevents edits
- [ ] Hide drawing → visibility toggle
- [ ] Clear all → confirms and deletes
- [ ] Switch symbol → loads different drawings
- [ ] Switch timeframe → filters correctly
- [ ] Offline mode → queues operations
- [ ] Concurrent edits → conflict resolution

---

## 10. Roadmap de Implementación

### Fase 1: Backend Foundation (1h)
1. Crear tabla `chart_drawings` en PostgreSQL
2. Implementar `ChartDrawingsController` con CRUD básico
3. Agregar rutas y middleware de autenticación
4. Test con Postman/Thunder Client

### Fase 2: Frontend Store (1h)
1. Crear `drawingsStore.ts` con Zustand
2. Implementar optimistic updates
3. Crear `useDrawings` hook con debounce
4. Test con React DevTools

### Fase 3: Integration (1h)
1. Modificar `ChartDrawingToolsPanel` para usar hook
2. Conectar eventos (add, update, delete)
3. Test end-to-end
4. Agregar notificaciones de error/éxito

---

## 11. Referencias

- **Componente Actual:** `apps/frontend/src/modules/trading/components/ChartDrawingToolsPanel.tsx` (420 líneas)
- **Código Backend:** `apps/backend/src/controllers/chart-drawings.controller.ts` (a crear)
- **Store:** `apps/frontend/src/stores/drawingsStore.ts` (a crear)
- **Hook:** `apps/frontend/src/modules/trading/hooks/useDrawings.ts` (a crear)
- **US:** `US-TRD-010-persistir-dibujos.md`

---

**Última actualización:** 2026-01-25
**Responsable:** Frontend Lead + Backend Engineer

