Spaces:
Sleeping
Sleeping
| # 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 <token>`.** | |
| --- | |
| ## 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 | | |