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>
1293 lines
34 KiB
Markdown
1293 lines
34 KiB
Markdown
---
|
|
id: "ET-INV-002"
|
|
title: "API REST Investment Accounts"
|
|
type: "Technical Specification"
|
|
status: "Done"
|
|
priority: "Alta"
|
|
epic: "OQI-004"
|
|
project: "trading-platform"
|
|
version: "1.0.0"
|
|
created_date: "2025-12-05"
|
|
updated_date: "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:**
|
|
```http
|
|
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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```http
|
|
GET /api/v1/investment/products/550e8400-e29b-41d4-a716-446655440000
|
|
Authorization: Bearer {token}
|
|
```
|
|
|
|
**Response 200:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```http
|
|
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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```http
|
|
GET /api/v1/investment/accounts
|
|
Authorization: Bearer {token}
|
|
|
|
Query Parameters:
|
|
- status: string (optional) - 'active', 'paused', 'closed'
|
|
- product_id: string (optional)
|
|
```
|
|
|
|
**Response 200:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```http
|
|
GET /api/v1/investment/accounts/750e8400-e29b-41d4-a716-446655440000
|
|
Authorization: Bearer {token}
|
|
```
|
|
|
|
**Response 200:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```http
|
|
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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```http
|
|
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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```http
|
|
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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```http
|
|
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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```http
|
|
GET /api/v1/investment/portfolio
|
|
Authorization: Bearer {token}
|
|
```
|
|
|
|
**Response 200:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```http
|
|
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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```http
|
|
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:**
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```yaml
|
|
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
|