# POSITIONS.md — Módulo de posiciones Simulador de capital virtual. Las posiciones no generan órdenes reales en Polymarket — son un tracking local de apuestas simuladas en euros. **Todos los endpoints requieren `Authorization: Bearer `.** --- ## Endpoints ### `POST /api/v1/positions` Abre una posición nueva en un mercado activo. **Body** ```json { "marketId": "559677", "outcome": "YES", "amountEur": 100 } ``` | Campo | Tipo | Descripción | |---|---|---| | `marketId` | string | ID del mercado (debe existir y estar `active`) | | `outcome` | `"YES"` \| `"NO"` | Lado de la apuesta | | `amountEur` | float > 0 | Importe a invertir en euros | **Respuesta `201`** ```json { "ok": true, "data": { "id": 1, "userId": 1, "marketId": "559677", "outcome": "YES", "amountEur": 100, "entryPrice": 0.0075, "currentPrice": 0.0075, "pnl": 0, "kellyFraction": 0.25, "status": "open", "openedAt": "2026-05-16T09:14:55.750Z", "closedAt": null, "market": { "id": "559677", "question": "Will Hillary Clinton win the 2028 Democratic presidential nomination?", "yesPrice": 0.0075, "noPrice": 0.9925, "status": "active" } } } ``` **Campos de respuesta relevantes** | Campo | Descripción | |---|---| | `entryPrice` | Precio en el momento de abrir (`yesPrice` o `noPrice` según `outcome`) | | `currentPrice` | Precio actual (actualizado por `updatePositionsPnL` cada 30s) | | `pnl` | Profit & Loss en EUR (negativo = pérdida) | | `kellyFraction` | Fracción de Kelly calculada (capada al 0.25) | | `status` | `"open"` \| `"closed"` | --- ### `GET /api/v1/positions` Lista todas las posiciones del usuario autenticado (abiertas y cerradas). **Query params** | Param | Tipo | Default | Descripción | |---|---|---|---| | `limit` | int (1-100) | `20` | Máximo de resultados | | `offset` | int | `0` | Paginación por offset | **Respuesta `200`** ```json { "ok": true, "data": [ { "id": 1, "userId": 1, "marketId": "559677", "outcome": "YES", "amountEur": 100, "entryPrice": 0.0075, "currentPrice": 0.0075, "pnl": 0, "kellyFraction": 0.25, "status": "closed", "openedAt": "2026-05-16T09:14:55.750Z", "closedAt": "2026-05-16T09:15:04.155Z", "market": { "id": "559677", "question": "Will Hillary Clinton win the 2028 Democratic presidential nomination?", "yesPrice": 0.0075, "noPrice": 0.9925, "status": "active" } } ] } ``` --- ### `DELETE /api/v1/positions/:id` Cierra una posición abierta. Calcula el P&L final con el precio actual. **Params** | Param | Tipo | Descripción | |---|---|---| | `id` | int | ID de la posición | **Respuesta `200`** ```json { "ok": true, "data": { "id": 1, "status": "closed", "closedAt": "2026-05-16T09:15:04.155Z", "pnl": -99.25 } } ``` **Errores** | HTTP | Código | Cuándo | |---|---|---| | `404` | `NOT_FOUND` | Posición no existe o no pertenece al usuario | | `409` | `CONFLICT` | La posición ya está cerrada | --- ## Criterio de Kelly `kellyFraction = (pWin - pLoss) / pWin` capado al 25% y nunca negativo. - `pWin` = precio del outcome elegido (`yesPrice` o `noPrice`) - `pLoss` = `1 - pWin` El resultado es informativo — el usuario decide cuánto invertir. --- ## Socket — actualización de P&L El job `updatePositionsPnL` recalcula `currentPrice` y `pnl` de todas las posiciones abiertas cada 30s. No emite evento de socket propio; el frontend puede re-fetch `GET /positions` tras recibir `market_update`. --- ## Ejemplos `curl` ```bash TOKEN=$(curl -s -X POST http://localhost:7860/api/v1/auth/login \ -H 'Content-Type: application/json' \ -d '{"email":"admin@polysignal.test","password":"Admin123!"}' | jq -r '.data.token') # Abrir posición curl -s -X POST http://localhost:7860/api/v1/positions \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"marketId":"559677","outcome":"YES","amountEur":50}' | jq # Listar posiciones curl -s -H "Authorization: Bearer $TOKEN" http://localhost:7860/api/v1/positions | jq # Cerrar posición (id=1) curl -s -X DELETE -H "Authorization: Bearer $TOKEN" http://localhost:7860/api/v1/positions/1 | jq ``` --- ## Códigos de error | HTTP | Código | Cuándo | |---|---|---| | `400` | `VALIDATION_ERROR` | Body inválido (outcome no es YES/NO, amountEur <= 0) | | `401` | `UNAUTHORIZED` | Sin token o token inválido | | `404` | `NOT_FOUND` | Mercado o posición no existe | | `409` | `CONFLICT` | Mercado no activo o posición ya cerrada | | `500` | `INTERNAL` | Error inesperado |