trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/especificaciones/ET-INV-002-api.md
rckrdmrd c1b5081208 feat(ml): Complete FASE 11 - BTCUSD update and comprehensive documentation alignment
ML Engine Updates:
- Updated BTCUSD with Polygon API data (2024-2025): 215,699 new records
- Re-trained all ML models: Attention (R²: 0.223), Base, Metamodel (87.3% confidence)
- Backtest results: +176.71R profit with aggressive_filter strategy

Documentation Consolidation:
- Created docs/99-analisis/_MAP.md index with 13 new analysis documents
- Consolidated inventories: removed duplicates from orchestration/inventarios/
- Updated ML_INVENTORY.yml with BTCUSD metrics and training results
- Added execution reports: FASE11-BTCUSD, correction issues, alignment validation

Architecture & Integration:
- Updated all module documentation with NEXUS v3.4 frontmatter
- Fixed _MAP.md indexes across all folders
- Updated orchestration plans and traces

Files: 229 changed, 5064 insertions(+), 1872 deletions(-)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 09:31:29 -06:00

34 KiB

id title type status priority epic project version created_date updated_date
ET-INV-002 API REST Investment Accounts Technical Specification Done Alta OQI-004 trading-platform 1.0.0 2025-12-05 2026-01-04

ET-INV-002: API REST Investment Accounts

Epic: OQI-004 Cuentas de Inversión Versión: 1.0 Fecha: 2025-12-05 Responsable: Requirements-Analyst


1. Descripción

Define los endpoints REST para la gestión completa de cuentas de inversión:

  • CRUD de productos de inversión
  • Gestión de cuentas de inversión
  • Depósitos y retiros
  • Consulta de portfolio y performance
  • Administración de solicitudes de retiro

2. Arquitectura de API

┌─────────────────────────────────────────────────────────────────┐
│                      Investment API Layer                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  ┌──────────────┐     ┌──────────────┐     ┌──────────────┐    │
│  │   Routes     │────►│ Controllers  │────►│   Services   │    │
│  └──────────────┘     └──────────────┘     └──────────────┘    │
│         │                     │                     │            │
│         │                     │                     ▼            │
│         │                     │            ┌──────────────┐     │
│         │                     │            │     DB       │     │
│         │                     │            │  (Postgres)  │     │
│         │                     │            └──────────────┘     │
│         │                     │                                  │
│         │                     ▼                                  │
│         │            ┌──────────────┐                            │
│         │            │ Middlewares  │                            │
│         │            │  - Auth      │                            │
│         │            │  - Validate  │                            │
│         │            │  - RateLimit │                            │
│         │            └──────────────┘                            │
│         │                                                         │
│         ▼                                                         │
│  ┌──────────────┐                                                │
│  │ Integration  │                                                │
│  │  - Stripe    │                                                │
│  │  - ML Engine │                                                │
│  └──────────────┘                                                │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

3. Endpoints Especificados

3.1 Productos de Inversión

GET /api/v1/investment/products

Lista todos los productos de inversión disponibles.

Request:

GET /api/v1/investment/products
Authorization: Bearer {token}

Query Parameters:
- status: string (optional) - 'active', 'paused', 'closed'
- agent_type: string (optional) - 'swing', 'day', 'scalping', 'arbitrage'
- risk_level: string (optional) - 'low', 'medium', 'high', 'very_high'
- limit: number (optional, default: 20)
- offset: number (optional, default: 0)

Response 200:

{
  "success": true,
  "data": {
    "products": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "name": "Swing Trader Pro",
        "description": "Agente de swing trading con estrategias de mediano plazo",
        "agent_type": "swing",
        "min_investment": 500.00,
        "max_investment": null,
        "performance_fee_percentage": 20.00,
        "target_annual_return": 35.50,
        "risk_level": "medium",
        "status": "active",
        "is_accepting_new_investors": true,
        "total_aum": 125000.00,
        "total_investors": 45,
        "created_at": "2025-01-15T10:00:00Z",
        "updated_at": "2025-01-20T14:30:00Z"
      }
    ],
    "pagination": {
      "total": 5,
      "limit": 20,
      "offset": 0,
      "has_more": false
    }
  }
}

Errors:

  • 401 Unauthorized: Token inválido o expirado
  • 500 Internal Server Error: Error en el servidor

GET /api/v1/investment/products/:id

Obtiene detalles de un producto específico.

Request:

GET /api/v1/investment/products/550e8400-e29b-41d4-a716-446655440000
Authorization: Bearer {token}

Response 200:

{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Swing Trader Pro",
    "description": "Agente de swing trading con estrategias de mediano plazo",
    "agent_type": "swing",
    "min_investment": 500.00,
    "max_investment": null,
    "performance_fee_percentage": 20.00,
    "target_annual_return": 35.50,
    "risk_level": "medium",
    "ml_agent_id": "ml-agent-swing-001",
    "ml_config": {
      "trading_pairs": ["BTC/USD", "ETH/USD"],
      "max_position_size": 0.1,
      "stop_loss_percentage": 2.5
    },
    "status": "active",
    "is_accepting_new_investors": true,
    "total_aum": 125000.00,
    "total_investors": 45,
    "created_at": "2025-01-15T10:00:00Z",
    "updated_at": "2025-01-20T14:30:00Z"
  }
}

Errors:

  • 404 Not Found: Producto no encontrado

POST /api/v1/investment/products (Admin Only)

Crea un nuevo producto de inversión.

Request:

POST /api/v1/investment/products
Authorization: Bearer {admin_token}
Content-Type: application/json

{
  "name": "Scalping Expert",
  "description": "Agente de scalping de alta frecuencia",
  "agent_type": "scalping",
  "min_investment": 2000.00,
  "max_investment": 50000.00,
  "performance_fee_percentage": 30.00,
  "target_annual_return": 60.00,
  "risk_level": "very_high",
  "ml_agent_id": "ml-agent-scalping-001",
  "ml_config": {
    "timeframe": "1m",
    "max_trades_per_day": 100
  }
}

Response 201:

{
  "success": true,
  "data": {
    "id": "650e8400-e29b-41d4-a716-446655440001",
    "name": "Scalping Expert",
    "status": "active",
    "created_at": "2025-01-21T09:00:00Z"
  }
}

Errors:

  • 400 Bad Request: Validación fallida
  • 403 Forbidden: No es administrador
  • 409 Conflict: ml_agent_id ya existe

3.2 Cuentas de Inversión

GET /api/v1/investment/accounts

Lista las cuentas de inversión del usuario autenticado.

Request:

GET /api/v1/investment/accounts
Authorization: Bearer {token}

Query Parameters:
- status: string (optional) - 'active', 'paused', 'closed'
- product_id: string (optional)

Response 200:

{
  "success": true,
  "data": {
    "accounts": [
      {
        "id": "750e8400-e29b-41d4-a716-446655440000",
        "product_id": "550e8400-e29b-41d4-a716-446655440000",
        "product_name": "Swing Trader Pro",
        "current_balance": 5250.75,
        "initial_investment": 5000.00,
        "total_deposited": 5000.00,
        "total_withdrawn": 0.00,
        "total_profit_distributed": 250.75,
        "total_return_percentage": 5.015,
        "annualized_return_percentage": 35.10,
        "status": "active",
        "opened_at": "2025-01-10T08:00:00Z"
      }
    ],
    "summary": {
      "total_invested": 5000.00,
      "total_current_value": 5250.75,
      "total_profit": 250.75,
      "total_return_percentage": 5.015
    }
  }
}

GET /api/v1/investment/accounts/:id

Obtiene detalles de una cuenta específica.

Request:

GET /api/v1/investment/accounts/750e8400-e29b-41d4-a716-446655440000
Authorization: Bearer {token}

Response 200:

{
  "success": true,
  "data": {
    "account": {
      "id": "750e8400-e29b-41d4-a716-446655440000",
      "product_id": "550e8400-e29b-41d4-a716-446655440000",
      "current_balance": 5250.75,
      "initial_investment": 5000.00,
      "total_deposited": 5000.00,
      "total_withdrawn": 0.00,
      "total_profit_distributed": 250.75,
      "total_return_percentage": 5.015,
      "annualized_return_percentage": 35.10,
      "status": "active",
      "opened_at": "2025-01-10T08:00:00Z"
    },
    "product": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "Swing Trader Pro",
      "agent_type": "swing",
      "risk_level": "medium",
      "performance_fee_percentage": 20.00
    },
    "recent_transactions": [
      {
        "id": "850e8400-e29b-41d4-a716-446655440000",
        "type": "deposit",
        "amount": 5000.00,
        "status": "completed",
        "created_at": "2025-01-10T08:00:00Z"
      }
    ]
  }
}

Errors:

  • 403 Forbidden: Cuenta no pertenece al usuario
  • 404 Not Found: Cuenta no encontrada

POST /api/v1/investment/accounts

Crea una nueva cuenta de inversión (depósito inicial).

Request:

POST /api/v1/investment/accounts
Authorization: Bearer {token}
Content-Type: application/json

{
  "product_id": "550e8400-e29b-41d4-a716-446655440000",
  "initial_investment": 5000.00,
  "payment_method_id": "pm_1234567890abcdef"
}

Response 201:

{
  "success": true,
  "data": {
    "account_id": "750e8400-e29b-41d4-a716-446655440000",
    "payment_intent": {
      "id": "pi_1234567890abcdef",
      "client_secret": "pi_1234567890abcdef_secret_xyz",
      "status": "requires_confirmation"
    }
  }
}

Errors:

  • 400 Bad Request: Validación fallida (ej: monto menor a min_investment)
  • 409 Conflict: Usuario ya tiene cuenta en este producto

3.3 Depósitos

POST /api/v1/investment/accounts/:id/deposit

Realiza un depósito adicional a una cuenta existente.

Request:

POST /api/v1/investment/accounts/750e8400-e29b-41d4-a716-446655440000/deposit
Authorization: Bearer {token}
Content-Type: application/json

{
  "amount": 1000.00,
  "payment_method_id": "pm_1234567890abcdef"
}

Response 200:

{
  "success": true,
  "data": {
    "transaction_id": "850e8400-e29b-41d4-a716-446655440001",
    "payment_intent": {
      "id": "pi_9876543210abcdef",
      "client_secret": "pi_9876543210abcdef_secret_abc",
      "status": "requires_confirmation"
    }
  }
}

Errors:

  • 400 Bad Request: Monto inválido
  • 403 Forbidden: Cuenta no pertenece al usuario
  • 404 Not Found: Cuenta no encontrada
  • 409 Conflict: Cuenta no está activa

3.4 Retiros

POST /api/v1/investment/accounts/:id/withdraw

Crea una solicitud de retiro.

Request:

POST /api/v1/investment/accounts/750e8400-e29b-41d4-a716-446655440000/withdraw
Authorization: Bearer {token}
Content-Type: application/json

{
  "amount": 500.00,
  "withdrawal_method": "bank_transfer",
  "destination_details": {
    "bank_name": "Bank of America",
    "account_number": "****1234",
    "routing_number": "026009593",
    "account_holder_name": "John Doe"
  }
}

Response 201:

{
  "success": true,
  "data": {
    "withdrawal_request_id": "950e8400-e29b-41d4-a716-446655440000",
    "status": "pending",
    "amount": 500.00,
    "estimated_processing_time": "2-5 business days",
    "requested_at": "2025-01-21T10:00:00Z"
  }
}

Errors:

  • 400 Bad Request: Monto excede balance disponible
  • 403 Forbidden: Cuenta no pertenece al usuario
  • 409 Conflict: Ya existe solicitud de retiro pendiente

GET /api/v1/investment/withdrawal-requests

Lista las solicitudes de retiro del usuario.

Request:

GET /api/v1/investment/withdrawal-requests
Authorization: Bearer {token}

Query Parameters:
- status: string (optional) - 'pending', 'approved', 'rejected', 'completed'
- limit: number (optional, default: 20)
- offset: number (optional, default: 0)

Response 200:

{
  "success": true,
  "data": {
    "requests": [
      {
        "id": "950e8400-e29b-41d4-a716-446655440000",
        "account_id": "750e8400-e29b-41d4-a716-446655440000",
        "amount": 500.00,
        "withdrawal_method": "bank_transfer",
        "status": "pending",
        "requested_at": "2025-01-21T10:00:00Z"
      }
    ],
    "pagination": {
      "total": 1,
      "limit": 20,
      "offset": 0
    }
  }
}

3.5 Portfolio y Performance

GET /api/v1/investment/portfolio

Obtiene el resumen completo del portfolio del usuario.

Request:

GET /api/v1/investment/portfolio
Authorization: Bearer {token}

Response 200:

{
  "success": true,
  "data": {
    "summary": {
      "total_invested": 10000.00,
      "total_current_value": 10850.50,
      "total_profit": 850.50,
      "total_return_percentage": 8.505,
      "annualized_return_percentage": 42.50
    },
    "accounts": [
      {
        "account_id": "750e8400-e29b-41d4-a716-446655440000",
        "product_name": "Swing Trader Pro",
        "agent_type": "swing",
        "current_balance": 5250.75,
        "invested": 5000.00,
        "profit": 250.75,
        "return_percentage": 5.015,
        "allocation_percentage": 48.40
      },
      {
        "account_id": "750e8400-e29b-41d4-a716-446655440001",
        "product_name": "Day Trader Elite",
        "agent_type": "day",
        "current_balance": 5599.75,
        "invested": 5000.00,
        "profit": 599.75,
        "return_percentage": 11.995,
        "allocation_percentage": 51.60
      }
    ],
    "allocation_by_risk": {
      "low": 0.00,
      "medium": 48.40,
      "high": 51.60,
      "very_high": 0.00
    }
  }
}

GET /api/v1/investment/accounts/:id/performance

Obtiene el historial de performance de una cuenta.

Request:

GET /api/v1/investment/accounts/750e8400-e29b-41d4-a716-446655440000/performance
Authorization: Bearer {token}

Query Parameters:
- period: string - 'week', 'month', 'quarter', 'year', 'all'
- start_date: string (optional) - ISO 8601 date
- end_date: string (optional) - ISO 8601 date

Response 200:

{
  "success": true,
  "data": {
    "account_id": "750e8400-e29b-41d4-a716-446655440000",
    "period": "month",
    "performance": [
      {
        "date": "2025-01-10",
        "opening_balance": 5000.00,
        "closing_balance": 5025.50,
        "daily_return": 25.50,
        "daily_return_percentage": 0.51,
        "cumulative_return": 25.50,
        "cumulative_return_percentage": 0.51
      },
      {
        "date": "2025-01-11",
        "opening_balance": 5025.50,
        "closing_balance": 5075.25,
        "daily_return": 49.75,
        "daily_return_percentage": 0.99,
        "cumulative_return": 75.25,
        "cumulative_return_percentage": 1.505
      }
    ],
    "statistics": {
      "total_days": 30,
      "winning_days": 22,
      "losing_days": 8,
      "best_day_return": 125.50,
      "worst_day_return": -45.20,
      "average_daily_return": 8.35,
      "volatility": 2.15
    }
  }
}

3.6 Transacciones

GET /api/v1/investment/transactions

Lista todas las transacciones del usuario.

Request:

GET /api/v1/investment/transactions
Authorization: Bearer {token}

Query Parameters:
- account_id: string (optional)
- type: string (optional) - 'deposit', 'withdrawal', 'profit_distribution', 'fee'
- status: string (optional) - 'pending', 'completed', 'failed', 'cancelled'
- start_date: string (optional)
- end_date: string (optional)
- limit: number (optional, default: 50)
- offset: number (optional, default: 0)

Response 200:

{
  "success": true,
  "data": {
    "transactions": [
      {
        "id": "850e8400-e29b-41d4-a716-446655440000",
        "account_id": "750e8400-e29b-41d4-a716-446655440000",
        "type": "deposit",
        "status": "completed",
        "amount": 5000.00,
        "balance_before": 0.00,
        "balance_after": 5000.00,
        "created_at": "2025-01-10T08:00:00Z",
        "processed_at": "2025-01-10T08:05:00Z"
      }
    ],
    "pagination": {
      "total": 15,
      "limit": 50,
      "offset": 0
    }
  }
}

4. Implementación Backend

4.1 Estructura de Archivos

src/
├── modules/
│   └── investment/
│       ├── investment.routes.ts
│       ├── investment.controller.ts
│       ├── investment.service.ts
│       ├── investment.repository.ts
│       ├── investment.validators.ts
│       └── investment.types.ts
├── middlewares/
│   ├── auth.middleware.ts
│   ├── validate.middleware.ts
│   └── rate-limit.middleware.ts
└── utils/
    ├── errors.ts
    └── response.ts

4.2 Routes

// src/modules/investment/investment.routes.ts

import { Router } from 'express';
import { InvestmentController } from './investment.controller';
import { authenticate, requireAdmin } from '../../middlewares/auth.middleware';
import { validate } from '../../middlewares/validate.middleware';
import { rateLimit } from '../../middlewares/rate-limit.middleware';
import {
  createProductSchema,
  createAccountSchema,
  depositSchema,
  withdrawalSchema,
} from './investment.validators';

const router = Router();
const controller = new InvestmentController();

// Products
router.get('/products', authenticate, controller.getProducts);
router.get('/products/:id', authenticate, controller.getProductById);
router.post('/products', authenticate, requireAdmin, validate(createProductSchema), controller.createProduct);
router.patch('/products/:id', authenticate, requireAdmin, controller.updateProduct);

// Accounts
router.get('/accounts', authenticate, controller.getAccounts);
router.get('/accounts/:id', authenticate, controller.getAccountById);
router.post('/accounts', authenticate, validate(createAccountSchema), rateLimit(5, 3600), controller.createAccount);

// Deposits
router.post('/accounts/:id/deposit', authenticate, validate(depositSchema), rateLimit(10, 3600), controller.deposit);

// Withdrawals
router.post('/accounts/:id/withdraw', authenticate, validate(withdrawalSchema), rateLimit(3, 3600), controller.withdraw);
router.get('/withdrawal-requests', authenticate, controller.getWithdrawalRequests);
router.patch('/withdrawal-requests/:id', authenticate, requireAdmin, controller.updateWithdrawalRequest);

// Portfolio
router.get('/portfolio', authenticate, controller.getPortfolio);
router.get('/accounts/:id/performance', authenticate, controller.getPerformance);

// Transactions
router.get('/transactions', authenticate, controller.getTransactions);

export default router;

4.3 Controller

// src/modules/investment/investment.controller.ts

import { Request, Response, NextFunction } from 'express';
import { InvestmentService } from './investment.service';
import { successResponse, errorResponse } from '../../utils/response';

export class InvestmentController {
  private service: InvestmentService;

  constructor() {
    this.service = new InvestmentService();
  }

  getProducts = async (req: Request, res: Response, next: NextFunction) => {
    try {
      const { status, agent_type, risk_level, limit = 20, offset = 0 } = req.query;

      const result = await this.service.getProducts({
        status: status as string,
        agent_type: agent_type as string,
        risk_level: risk_level as string,
        limit: Number(limit),
        offset: Number(offset),
      });

      return successResponse(res, result, 200);
    } catch (error) {
      next(error);
    }
  };

  getProductById = async (req: Request, res: Response, next: NextFunction) => {
    try {
      const { id } = req.params;
      const product = await this.service.getProductById(id);

      if (!product) {
        return errorResponse(res, 'Product not found', 404);
      }

      return successResponse(res, product, 200);
    } catch (error) {
      next(error);
    }
  };

  createAccount = async (req: Request, res: Response, next: NextFunction) => {
    try {
      const userId = req.user!.id;
      const { product_id, initial_investment, payment_method_id } = req.body;

      const result = await this.service.createAccount({
        user_id: userId,
        product_id,
        initial_investment,
        payment_method_id,
      });

      return successResponse(res, result, 201);
    } catch (error) {
      next(error);
    }
  };

  deposit = async (req: Request, res: Response, next: NextFunction) => {
    try {
      const userId = req.user!.id;
      const { id: accountId } = req.params;
      const { amount, payment_method_id } = req.body;

      const result = await this.service.deposit({
        user_id: userId,
        account_id: accountId,
        amount,
        payment_method_id,
      });

      return successResponse(res, result, 200);
    } catch (error) {
      next(error);
    }
  };

  withdraw = async (req: Request, res: Response, next: NextFunction) => {
    try {
      const userId = req.user!.id;
      const { id: accountId } = req.params;
      const { amount, withdrawal_method, destination_details } = req.body;

      const result = await this.service.createWithdrawalRequest({
        user_id: userId,
        account_id: accountId,
        amount,
        withdrawal_method,
        destination_details,
      });

      return successResponse(res, result, 201);
    } catch (error) {
      next(error);
    }
  };

  getPortfolio = async (req: Request, res: Response, next: NextFunction) => {
    try {
      const userId = req.user!.id;
      const portfolio = await this.service.getPortfolio(userId);

      return successResponse(res, portfolio, 200);
    } catch (error) {
      next(error);
    }
  };

  getPerformance = async (req: Request, res: Response, next: NextFunction) => {
    try {
      const userId = req.user!.id;
      const { id: accountId } = req.params;
      const { period, start_date, end_date } = req.query;

      const performance = await this.service.getPerformance({
        user_id: userId,
        account_id: accountId,
        period: period as string,
        start_date: start_date as string,
        end_date: end_date as string,
      });

      return successResponse(res, performance, 200);
    } catch (error) {
      next(error);
    }
  };

  // ... otros métodos
}

4.4 Service

// src/modules/investment/investment.service.ts

import { InvestmentRepository } from './investment.repository';
import { StripeService } from '../payments/stripe.service';
import { AppError } from '../../utils/errors';
import { CreateAccountDto, DepositDto, WithdrawalDto } from './investment.types';

export class InvestmentService {
  private repository: InvestmentRepository;
  private stripeService: StripeService;

  constructor() {
    this.repository = new InvestmentRepository();
    this.stripeService = new StripeService();
  }

  async getProducts(filters: any) {
    const { products, total } = await this.repository.getProducts(filters);

    return {
      products,
      pagination: {
        total,
        limit: filters.limit,
        offset: filters.offset,
        has_more: total > filters.offset + filters.limit,
      },
    };
  }

  async createAccount(data: CreateAccountDto) {
    // Validar producto
    const product = await this.repository.getProductById(data.product_id);
    if (!product) {
      throw new AppError('Product not found', 404);
    }

    if (!product.is_accepting_new_investors) {
      throw new AppError('Product is not accepting new investors', 400);
    }

    // Validar monto mínimo
    if (data.initial_investment < product.min_investment) {
      throw new AppError(
        `Minimum investment is ${product.min_investment}`,
        400
      );
    }

    // Validar monto máximo
    if (
      product.max_investment &&
      data.initial_investment > product.max_investment
    ) {
      throw new AppError(
        `Maximum investment is ${product.max_investment}`,
        400
      );
    }

    // Verificar si ya existe cuenta
    const existingAccount = await this.repository.getAccountByUserAndProduct(
      data.user_id,
      data.product_id
    );

    if (existingAccount) {
      throw new AppError('Account already exists for this product', 409);
    }

    // Crear Payment Intent en Stripe
    const paymentIntent = await this.stripeService.createPaymentIntent({
      amount: data.initial_investment,
      currency: 'usd',
      payment_method: data.payment_method_id,
      metadata: {
        type: 'investment_deposit',
        product_id: data.product_id,
        user_id: data.user_id,
      },
    });

    // Crear cuenta (pendiente de confirmación de pago)
    const account = await this.repository.createAccount({
      user_id: data.user_id,
      product_id: data.product_id,
      initial_investment: data.initial_investment,
    });

    // Crear transacción pendiente
    await this.repository.createTransaction({
      account_id: account.id,
      user_id: data.user_id,
      type: 'deposit',
      amount: data.initial_investment,
      balance_before: 0,
      stripe_payment_intent_id: paymentIntent.id,
      status: 'pending',
    });

    return {
      account_id: account.id,
      payment_intent: {
        id: paymentIntent.id,
        client_secret: paymentIntent.client_secret,
        status: paymentIntent.status,
      },
    };
  }

  async deposit(data: DepositDto) {
    // Validar cuenta
    const account = await this.repository.getAccountById(data.account_id);
    if (!account) {
      throw new AppError('Account not found', 404);
    }

    if (account.user_id !== data.user_id) {
      throw new AppError('Forbidden', 403);
    }

    if (account.status !== 'active') {
      throw new AppError('Account is not active', 409);
    }

    // Crear Payment Intent
    const paymentIntent = await this.stripeService.createPaymentIntent({
      amount: data.amount,
      currency: 'usd',
      payment_method: data.payment_method_id,
      metadata: {
        type: 'investment_deposit',
        account_id: data.account_id,
        user_id: data.user_id,
      },
    });

    // Crear transacción pendiente
    const transaction = await this.repository.createTransaction({
      account_id: data.account_id,
      user_id: data.user_id,
      type: 'deposit',
      amount: data.amount,
      balance_before: account.current_balance,
      stripe_payment_intent_id: paymentIntent.id,
      status: 'pending',
    });

    return {
      transaction_id: transaction.id,
      payment_intent: {
        id: paymentIntent.id,
        client_secret: paymentIntent.client_secret,
        status: paymentIntent.status,
      },
    };
  }

  async getPortfolio(userId: string) {
    const accounts = await this.repository.getAccountsByUser(userId);

    const totalInvested = accounts.reduce((sum, acc) => sum + acc.total_deposited, 0);
    const totalCurrentValue = accounts.reduce((sum, acc) => sum + acc.current_balance, 0);
    const totalProfit = totalCurrentValue - totalInvested;
    const totalReturnPercentage = totalInvested > 0 ? (totalProfit / totalInvested) * 100 : 0;

    // Calcular allocación por riesgo
    const allocationByRisk = accounts.reduce((acc, account) => {
      const percentage = (account.current_balance / totalCurrentValue) * 100;
      acc[account.product.risk_level] = (acc[account.product.risk_level] || 0) + percentage;
      return acc;
    }, {} as Record<string, number>);

    return {
      summary: {
        total_invested: totalInvested,
        total_current_value: totalCurrentValue,
        total_profit: totalProfit,
        total_return_percentage: totalReturnPercentage,
      },
      accounts: accounts.map((acc) => ({
        account_id: acc.id,
        product_name: acc.product.name,
        agent_type: acc.product.agent_type,
        current_balance: acc.current_balance,
        invested: acc.total_deposited,
        profit: acc.current_balance - acc.total_deposited,
        return_percentage: acc.total_return_percentage,
        allocation_percentage: (acc.current_balance / totalCurrentValue) * 100,
      })),
      allocation_by_risk: allocationByRisk,
    };
  }

  // ... otros métodos
}

5. Validaciones

5.1 Schemas Zod

// src/modules/investment/investment.validators.ts

import { z } from 'zod';

export const createProductSchema = z.object({
  name: z.string().min(3).max(100),
  description: z.string().optional(),
  agent_type: z.enum(['swing', 'day', 'scalping', 'arbitrage']),
  min_investment: z.number().positive(),
  max_investment: z.number().positive().optional(),
  performance_fee_percentage: z.number().min(0).max(100),
  target_annual_return: z.number().optional(),
  risk_level: z.enum(['low', 'medium', 'high', 'very_high']),
  ml_agent_id: z.string().min(1),
  ml_config: z.record(z.any()).optional(),
});

export const createAccountSchema = z.object({
  product_id: z.string().uuid(),
  initial_investment: z.number().positive(),
  payment_method_id: z.string().min(1),
});

export const depositSchema = z.object({
  amount: z.number().positive(),
  payment_method_id: z.string().min(1),
});

export const withdrawalSchema = z.object({
  amount: z.number().positive(),
  withdrawal_method: z.enum(['bank_transfer', 'stripe_payout']),
  destination_details: z.record(z.any()),
});

6. Seguridad

6.1 Rate Limiting

// Límites por endpoint
const RATE_LIMITS = {
  createAccount: { max: 5, window: 3600 }, // 5 cuentas/hora
  deposit: { max: 10, window: 3600 }, // 10 depósitos/hora
  withdraw: { max: 3, window: 3600 }, // 3 retiros/hora
};

6.2 Autenticación

  • Todos los endpoints requieren JWT válido
  • Endpoints de admin requieren rol admin
  • Verificación de ownership para acceso a cuentas

7. Configuración

7.1 Variables de Entorno

# API
PORT=3000
API_PREFIX=/api/v1

# Investment
INVESTMENT_MIN_DEPOSIT=50.00
INVESTMENT_MIN_WITHDRAWAL=50.00
INVESTMENT_MAX_WITHDRAWAL_PENDING=5

# Rate Limits
RATE_LIMIT_WINDOW_MS=3600000
RATE_LIMIT_MAX_REQUESTS=100

8. Testing

8.1 Test de Endpoints

// tests/investment/accounts.test.ts

import request from 'supertest';
import app from '../../src/app';

describe('Investment Accounts API', () => {
  let authToken: string;
  let productId: string;

  beforeAll(async () => {
    // Setup: autenticar y crear producto
    authToken = await getAuthToken();
    productId = await createTestProduct();
  });

  describe('POST /api/v1/investment/accounts', () => {
    it('should create new account with valid data', async () => {
      const response = await request(app)
        .post('/api/v1/investment/accounts')
        .set('Authorization', `Bearer ${authToken}`)
        .send({
          product_id: productId,
          initial_investment: 5000,
          payment_method_id: 'pm_test_123',
        });

      expect(response.status).toBe(201);
      expect(response.body.success).toBe(true);
      expect(response.body.data).toHaveProperty('account_id');
      expect(response.body.data.payment_intent).toHaveProperty('client_secret');
    });

    it('should reject investment below minimum', async () => {
      const response = await request(app)
        .post('/api/v1/investment/accounts')
        .set('Authorization', `Bearer ${authToken}`)
        .send({
          product_id: productId,
          initial_investment: 50, // Menor al mínimo
          payment_method_id: 'pm_test_123',
        });

      expect(response.status).toBe(400);
    });

    it('should reject duplicate account', async () => {
      // Intentar crear segunda cuenta en mismo producto
      const response = await request(app)
        .post('/api/v1/investment/accounts')
        .set('Authorization', `Bearer ${authToken}`)
        .send({
          product_id: productId,
          initial_investment: 5000,
          payment_method_id: 'pm_test_123',
        });

      expect(response.status).toBe(409);
    });
  });

  describe('GET /api/v1/investment/portfolio', () => {
    it('should return user portfolio', async () => {
      const response = await request(app)
        .get('/api/v1/investment/portfolio')
        .set('Authorization', `Bearer ${authToken}`);

      expect(response.status).toBe(200);
      expect(response.body.data).toHaveProperty('summary');
      expect(response.body.data).toHaveProperty('accounts');
      expect(response.body.data.summary).toHaveProperty('total_invested');
    });
  });
});

9. Documentación OpenAPI

openapi: 3.0.0
info:
  title: Trading Platform - Investment API
  version: 1.0.0
  description: API para gestión de cuentas de inversión

paths:
  /api/v1/investment/products:
    get:
      summary: Lista productos de inversión
      tags: [Products]
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Lista de productos
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductsResponse'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  schemas:
    ProductsResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            products:
              type: array
              items:
                $ref: '#/components/schemas/Product'

10. Referencias

  • Stripe Payment Intents API
  • Express.js Best Practices
  • Zod Validation Library
  • PostgreSQL Transaction Management