Spaces:
Sleeping
Sleeping
| # MARKETS.md — Módulo de mercados | |
| Referencia para el frontend: contrato HTTP, mapping de datos de Polymarket, evento socket y errores. | |
| --- | |
| ## Variables de entorno requeridas | |
| ``` | |
| DATABASE_URL=file:./backend/prisma/polysignal.db | |
| PORT=7860 | |
| ``` | |
| No se necesita clave de API para Polymarket Gamma (pública). | |
| --- | |
| ## Endpoints | |
| ### `GET /api/v1/markets` | |
| Lista paginada de mercados activos sincronizados desde Polymarket. **No requiere autenticación.** | |
| **Query params** | |
| | Param | Tipo | Default | Descripción | | |
| |---|---|---|---| | |
| | `limit` | int (1-100) | `20` | Máximo de resultados | | |
| | `offset` | int | `0` | Paginación por offset | | |
| | `category` | string | — | Filtro: `politics` \| `crypto` \| `economics` \| `sports` | | |
| | `status` | string | `active` | Filtro: `active` \| `closed` \| `resolved` | | |
| **Respuesta `200`** | |
| ```json | |
| { | |
| "ok": true, | |
| "data": [ | |
| { | |
| "id": "559677", | |
| "question": "Will Hillary Clinton win the 2028 Democratic presidential nomination?", | |
| "category": null, | |
| "countryCode": null, | |
| "yesPrice": 0.0075, | |
| "noPrice": 0.9925, | |
| "volumeEur": 38608906.44, | |
| "liquidityEur": 2301398.64, | |
| "status": "active", | |
| "closesAt": "2028-11-07T00:00:00.000Z", | |
| "lastSynced": "2026-05-16T09:38:30.204Z" | |
| } | |
| ], | |
| "meta": { | |
| "total": 100, | |
| "limit": 1, | |
| "offset": 0 | |
| } | |
| } | |
| ``` | |
| > `category` y `countryCode` pueden ser `null` si Polymarket no los proporciona. | |
| --- | |
| ### `GET /api/v1/markets/:id` | |
| Detalle de un mercado por su ID de Polymarket. **No requiere autenticación.** | |
| **Params** | |
| | Param | Tipo | Descripción | | |
| |---|---|---| | |
| | `id` | string | ID numérico de Polymarket (ej. `559677`) | | |
| **Respuesta `200`** | |
| ```json | |
| { | |
| "ok": true, | |
| "data": { | |
| "id": "559677", | |
| "question": "Will Hillary Clinton win the 2028 Democratic presidential nomination?", | |
| "category": null, | |
| "countryCode": null, | |
| "yesPrice": 0.0075, | |
| "noPrice": 0.9925, | |
| "volumeEur": 38608906.44, | |
| "liquidityEur": 2301398.64, | |
| "status": "active", | |
| "closesAt": "2028-11-07T00:00:00.000Z", | |
| "lastSynced": "2026-05-16T09:38:30.204Z" | |
| } | |
| } | |
| ``` | |
| **Respuesta `404`** | |
| ```json | |
| { | |
| "ok": false, | |
| "error": { "code": "NOT_FOUND", "message": "Market not found" } | |
| } | |
| ``` | |
| --- | |
| ### `GET /api/v1/markets/:id/signal` | |
| Señal AI más reciente para un mercado. Ver [SIGNALS.md](SIGNALS.md) para el contrato completo. | |
| --- | |
| ## Mapping Polymarket Gamma API → `Market` | |
| URL de origen: `https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=100` | |
| | Campo Gamma API | Campo `Market` | Transformación | | |
| |---|---|---| | |
| | `id` | `id` | String (ID numérico de Polymarket) | | |
| | `question` | `question` | Directo | | |
| | `category` | `category` | Minúsculas; `null` si Gamma no lo envía | | |
| | — | `countryCode` | `null` por defecto (no proporcionado por Gamma) | | |
| | `outcomePrices[0]` | `yesPrice` | `parseFloat`; rango 0.0–1.0 | | |
| | `outcomePrices[1]` | `noPrice` | `parseFloat`; rango 0.0–1.0 | | |
| | `volume` | `volumeEur` | `parseFloat(volume) * 0.93` (USD→EUR tasa fija) | | |
| | `liquidity` | `liquidityEur` | Igual que `volumeEur` | | |
| | `active` + `closed` + `archived` | `status` | `active=true → "active"`, `closed=true → "closed"`, `archived=true → "resolved"` | | |
| | `endDateIso` | `closesAt` | `new Date(endDateIso)` | | |
| | — | `lastSynced` | `new Date()` en el momento del upsert | | |
| La sincronización usa `prisma.market.upsert({ where: { id }, ... })` para evitar duplicados. | |
| --- | |
| ## Socket — evento `market_update` | |
| Emitido por `src/socket/broadcaster.js` cada 30 s (tras `syncMarkets`). | |
| **Nombre del evento:** `market_update` | |
| **Payload** | |
| ```json | |
| { | |
| "marketId": "0x1a2b...", | |
| "yesPrice": 0.63, | |
| "noPrice": 0.37, | |
| "volumeEur": 125000.00 | |
| } | |
| ``` | |
| **Uso en el frontend (Socket.io client)** | |
| ```js | |
| import { io } from 'socket.io-client'; | |
| const socket = io('http://localhost:7860'); | |
| socket.on('market_update', ({ marketId, yesPrice, noPrice }) => { | |
| // actualizar estado local del mercado | |
| }); | |
| ``` | |
| En producción (HF Spaces) el frontend y backend comparten origen → usar `io()` sin URL. | |
| --- | |
| ## Ejemplos `curl` | |
| ```bash | |
| # Listar mercados (primeros 5) | |
| curl "http://localhost:7860/api/v1/markets?limit=5" | |
| # Filtrar por estado | |
| curl "http://localhost:7860/api/v1/markets?status=active&limit=10" | |
| # Detalle de un mercado | |
| curl "http://localhost:7860/api/v1/markets/559677" | |
| # Señal AI del mercado | |
| curl "http://localhost:7860/api/v1/markets/559677/signal" | |
| ``` | |
| --- | |
| ## Códigos de error relevantes | |
| | HTTP | Código | Cuándo | | |
| |---|---|---| | |
| | `400` | `VALIDATION_ERROR` | Parámetro inválido (ej. `limit` > 100) | | |
| | `404` | `NOT_FOUND` | ID de mercado no existe en DB | | |
| | `500` | `INTERNAL` | Error inesperado del servidor | | |
| Los endpoints de markets **no requieren autenticación** — son datos públicos. | |