Spaces:
Sleeping
Sleeping
Jose Salazar commited on
Commit ·
714ef9d
1
Parent(s): 94ad3aa
Integracion parcial de backend con frontend, se anexa las capitales y mapa de arnau
Browse files- backend/src/app.js +6 -1
- backend/src/markets/polymarket.client.js +156 -3
- backend/src/signals/aiPipeline.js +12 -12
- backend/src/signals/signals.controller.js +7 -0
- backend/src/signals/signals.repository.js +16 -0
- backend/src/signals/signals.routes.js +4 -0
- backend/src/signals/signals.service.js +6 -0
- backend/src/stats/stats.routes.js +42 -0
- frontend/dist/assets/index-B7Y-b3uS.js +0 -0
- frontend/dist/assets/index-DUFVKRJi.js +0 -0
- frontend/dist/index.html +1 -1
- frontend/src/api.js +64 -2
- frontend/src/app.js +101 -214
- frontend/src/capitals.js +53 -0
- frontend/src/capitals.json +195 -0
- frontend/src/map.js +18 -27
- frontend/src/simulator.js +10 -22
backend/src/app.js
CHANGED
|
@@ -33,6 +33,7 @@ import signalsRoutes from './signals/signals.routes.js';
|
|
| 33 |
import positionsRoutes from './positions/positions.routes.js';
|
| 34 |
import watchlistRoutes from './watchlist/watchlist.routes.js';
|
| 35 |
import alertsRoutes from './alerts/alerts.routes.js';
|
|
|
|
| 36 |
import { notFound } from './middlewares/notFound.js';
|
| 37 |
import { errorHandler } from './middlewares/errorHandler.js';
|
| 38 |
|
|
@@ -40,10 +41,13 @@ const app = express();
|
|
| 40 |
|
| 41 |
app.use(helmet());
|
| 42 |
app.use(cors({ origin: config.CORS_ORIGIN, credentials: true }));
|
|
|
|
|
|
|
|
|
|
| 43 |
app.use(
|
| 44 |
rateLimit({
|
| 45 |
windowMs: 15 * 60 * 1000,
|
| 46 |
-
max:
|
| 47 |
standardHeaders: true,
|
| 48 |
legacyHeaders: false,
|
| 49 |
message: { ok: false, error: { code: 'TOO_MANY_REQUESTS', message: 'Rate limit exceeded' } },
|
|
@@ -58,6 +62,7 @@ app.use('/api/v1/markets', signalsRoutes);
|
|
| 58 |
app.use('/api/v1/positions', positionsRoutes);
|
| 59 |
app.use('/api/v1/watchlist', watchlistRoutes);
|
| 60 |
app.use('/api/v1/alerts', alertsRoutes);
|
|
|
|
| 61 |
|
| 62 |
app.use(notFound);
|
| 63 |
app.use(errorHandler);
|
|
|
|
| 33 |
import positionsRoutes from './positions/positions.routes.js';
|
| 34 |
import watchlistRoutes from './watchlist/watchlist.routes.js';
|
| 35 |
import alertsRoutes from './alerts/alerts.routes.js';
|
| 36 |
+
import statsRoutes from './stats/stats.routes.js';
|
| 37 |
import { notFound } from './middlewares/notFound.js';
|
| 38 |
import { errorHandler } from './middlewares/errorHandler.js';
|
| 39 |
|
|
|
|
| 41 |
|
| 42 |
app.use(helmet());
|
| 43 |
app.use(cors({ origin: config.CORS_ORIGIN, credentials: true }));
|
| 44 |
+
|
| 45 |
+
// Rate limit: muy permisivo en desarrollo, restrictivo en producción
|
| 46 |
+
const rateLimitMax = config.NODE_ENV === 'production' ? 200 : 5000;
|
| 47 |
app.use(
|
| 48 |
rateLimit({
|
| 49 |
windowMs: 15 * 60 * 1000,
|
| 50 |
+
max: rateLimitMax,
|
| 51 |
standardHeaders: true,
|
| 52 |
legacyHeaders: false,
|
| 53 |
message: { ok: false, error: { code: 'TOO_MANY_REQUESTS', message: 'Rate limit exceeded' } },
|
|
|
|
| 62 |
app.use('/api/v1/positions', positionsRoutes);
|
| 63 |
app.use('/api/v1/watchlist', watchlistRoutes);
|
| 64 |
app.use('/api/v1/alerts', alertsRoutes);
|
| 65 |
+
app.use('/api/v1/stats', statsRoutes);
|
| 66 |
|
| 67 |
app.use(notFound);
|
| 68 |
app.use(errorHandler);
|
backend/src/markets/polymarket.client.js
CHANGED
|
@@ -37,13 +37,166 @@ function parsePrices(outcomePrices) {
|
|
| 37 |
}
|
| 38 |
}
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
export function mapMarket(raw) {
|
| 41 |
const { yesPrice, noPrice } = parsePrices(raw.outcomePrices);
|
|
|
|
|
|
|
|
|
|
| 42 |
return {
|
| 43 |
id: String(raw.id),
|
| 44 |
-
question
|
| 45 |
-
category:
|
| 46 |
-
countryCode:
|
| 47 |
yesPrice,
|
| 48 |
noPrice,
|
| 49 |
volumeEur: raw.volume != null ? parseFloat(raw.volume) * USD_TO_EUR : null,
|
|
|
|
| 37 |
}
|
| 38 |
}
|
| 39 |
|
| 40 |
+
function inferCategory(question, eventTitle = '') {
|
| 41 |
+
const text = `${question} ${eventTitle}`.toLowerCase();
|
| 42 |
+
|
| 43 |
+
const rules = [
|
| 44 |
+
{ keywords: ['bitcoin', 'btc', 'ethereum', 'eth', 'crypto', 'blockchain', 'solana', 'cardano', 'altcoin', 'defi', 'nft'], category: 'cripto' },
|
| 45 |
+
{ keywords: ['fed', 'ecb', 'rate', 'interest', 'inflation', 'gdp', 'recession', 'economy', 'tariff', 'trade', 'dollar', 'euro', 'yen', 'bank', 'finance', 'stock market', 'sp500', 'nasdaq', 'unemployment', 'cpi', 'ppi'], category: 'economía' },
|
| 46 |
+
{ keywords: ['trump', 'biden', 'election', 'president', 'democrat', 'republican', 'congress', 'senate', 'house', 'vote', 'impeach', 'nominee', 'primary', 'governor', 'mayor', 'political', 'politics', 'campaign'], category: 'política' },
|
| 47 |
+
{ keywords: ['war', 'ukraine', 'russia', 'putin', 'china', 'xi', 'iran', 'israel', 'gaza', 'north korea', 'taiwan', 'invasion', 'missile', 'nuclear', 'sanctions', 'diplomatic', 'embassy', 'conflict'], category: 'geopolítica' },
|
| 48 |
+
{ keywords: ['super bowl', 'world cup', 'olympics', 'nba', 'nfl', 'mlb', 'soccer', 'football', 'tennis', 'golf', 'mvp', 'championship', 'fifa', 'uefa', 'premier league', 'playoff'], category: 'deportes' },
|
| 49 |
+
{ keywords: ['album', 'movie', 'oscar', 'grammy', 'emmy', 'hollywood', 'actor', 'singer', 'celebrity', 'gta', 'video game', 'song', 'chart', 'streaming', 'netflix', 'disney', 'marvel', 'rockstar'], category: 'entretenimiento' },
|
| 50 |
+
{ keywords: ['ai', 'spacex', 'mars', 'rocket', 'vaccine', 'climate', 'covid', 'pandemic', 'tesla', 'elon', 'neuralink', 'fusion', 'crispr'], category: 'ciencia' },
|
| 51 |
+
];
|
| 52 |
+
|
| 53 |
+
for (const rule of rules) {
|
| 54 |
+
if (rule.keywords.some((kw) => text.includes(kw))) {
|
| 55 |
+
return rule.category;
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
return 'general';
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
function inferCountryCode(question, eventTitle = '') {
|
| 63 |
+
// Envolver con espacios para evitar coincidencias parciales de substrings
|
| 64 |
+
const text = ` ${question} ${eventTitle} `.toLowerCase();
|
| 65 |
+
|
| 66 |
+
const rules = [
|
| 67 |
+
// Estados Unidos
|
| 68 |
+
{ keywords: [' usa ', ' us ', ' america ', ' american ', ' trump ', ' biden ', ' clinton ', ' obama ', ' harris ', ' pence ', ' sanders ', ' warren ', ' mcconnell ', ' pelosi ', ' schumer ', ' congress ', ' senate ', ' house of representatives ', ' fed ', ' super bowl ', ' nba ', ' nfl ', ' mlb ', ' nasdaq ', ' sp500 ', ' hollywood ', ' california ', ' new york ', ' texas ', ' florida ', ' white house ', ' pentagon ', ' supreme court ', ' capitol ', ' governor of ', ' democratic nomination ', ' republican nomination ', ' presidential '], code: 'US' },
|
| 69 |
+
// Reino Unido
|
| 70 |
+
{ keywords: [' uk ', ' britain ', ' british ', ' england ', ' london ', ' brexit ', ' boe ', ' pound ', ' sterling ', ' scotland ', ' wales ', ' king charles ', ' prime minister ', ' parliament ', ' westminster ', ' tory ', ' labour party '], code: 'GB' },
|
| 71 |
+
// Alemania
|
| 72 |
+
{ keywords: [' germany ', ' german ', ' merkel ', ' scholz ', ' berlin ', ' bundestag ', ' deutsche '], code: 'DE' },
|
| 73 |
+
// Francia
|
| 74 |
+
{ keywords: [' france ', ' french ', ' macron ', ' paris ', ' le pen ', ' élysée '], code: 'FR' },
|
| 75 |
+
// Italia
|
| 76 |
+
{ keywords: [' italy ', ' italian ', ' meloni ', ' rome ', ' berlusconi '], code: 'IT' },
|
| 77 |
+
// España
|
| 78 |
+
{ keywords: [' spain ', ' spanish ', ' sánchez ', ' madrid ', ' catalonia ', ' catalan '], code: 'ES' },
|
| 79 |
+
// China
|
| 80 |
+
{ keywords: [' china ', ' chinese ', ' xi jinping ', ' beijing ', ' shanghai ', ' hong kong ', ' taiwan ', ' yuan ', ' alibaba ', ' byd '], code: 'CN' },
|
| 81 |
+
// Rusia
|
| 82 |
+
{ keywords: [' russia ', ' russian ', ' putin ', ' moscow ', ' kremlin ', ' ruble '], code: 'RU' },
|
| 83 |
+
// India
|
| 84 |
+
{ keywords: [' india ', ' indian ', ' modi ', ' mumbai ', ' delhi ', ' rupee ', ' bjp '], code: 'IN' },
|
| 85 |
+
// Brasil
|
| 86 |
+
{ keywords: [' brazil ', ' brazilian ', ' brasil ', ' lula ', ' bolsonaro ', ' real '], code: 'BR' },
|
| 87 |
+
// Japón
|
| 88 |
+
{ keywords: [' japan ', ' japanese ', ' tokyo ', ' boj ', ' yen ', ' nikkei ', ' suzuki '], code: 'JP' },
|
| 89 |
+
// Canadá
|
| 90 |
+
{ keywords: [' canada ', ' canadian ', ' trudeau ', ' toronto ', ' loonie '], code: 'CA' },
|
| 91 |
+
// Ucrania
|
| 92 |
+
{ keywords: [' ukraine ', ' ukrainian ', ' kyiv ', ' zelensky '], code: 'UA' },
|
| 93 |
+
// Israel
|
| 94 |
+
{ keywords: [' israel ', ' israeli ', ' gaza ', ' netanyahu ', ' palestine ', ' palestinian ', ' hamas '], code: 'IL' },
|
| 95 |
+
// Irán
|
| 96 |
+
{ keywords: [' iran ', ' iranian ', ' tehran ', ' ayatollah '], code: 'IR' },
|
| 97 |
+
// Corea
|
| 98 |
+
{ keywords: [' korea ', ' north korea ', ' south korea ', ' korean ', ' seoul ', ' kim jong '], code: 'KR' },
|
| 99 |
+
// Australia
|
| 100 |
+
{ keywords: [' australia ', ' australian ', ' sydney ', ' rba ', ' aud '], code: 'AU' },
|
| 101 |
+
// México
|
| 102 |
+
{ keywords: [' mexico ', ' mexican ', ' peso ', ' amlo ', ' mexican president '], code: 'MX' },
|
| 103 |
+
// Turquía
|
| 104 |
+
{ keywords: [' turkey ', ' turkish ', ' erdogan ', ' lira ', ' istanbul '], code: 'TR' },
|
| 105 |
+
// Arabia Saudita
|
| 106 |
+
{ keywords: [' saudi ', ' saudi arabia ', ' riyadh ', ' aramco '], code: 'SA' },
|
| 107 |
+
// Sudáfrica
|
| 108 |
+
{ keywords: [' south africa ', ' south african ', ' johannesburg ', ' rand '], code: 'ZA' },
|
| 109 |
+
// Argentina
|
| 110 |
+
{ keywords: [' argentina ', ' argentinian ', ' milei ', ' buenos aires ', ' peso argentino '], code: 'AR' },
|
| 111 |
+
// Uzbekistán
|
| 112 |
+
{ keywords: [' uzbekistan ', ' uzbek '], code: 'UZ' },
|
| 113 |
+
// Nueva Zelanda
|
| 114 |
+
{ keywords: [' new zealand ', ' kiwi '], code: 'NZ' },
|
| 115 |
+
// Países Bajos
|
| 116 |
+
{ keywords: [' netherlands ', ' dutch ', ' amsterdam ', ' rutte '], code: 'NL' },
|
| 117 |
+
// Polonia
|
| 118 |
+
{ keywords: [' poland ', ' polish ', ' warsaw ', ' duda '], code: 'PL' },
|
| 119 |
+
// Suiza
|
| 120 |
+
{ keywords: [' switzerland ', ' swiss ', ' zurich ', ' geneva ', ' franc '], code: 'CH' },
|
| 121 |
+
// Suecia
|
| 122 |
+
{ keywords: [' sweden ', ' swedish ', ' stockholm '], code: 'SE' },
|
| 123 |
+
// Noruega
|
| 124 |
+
{ keywords: [' norway ', ' norwegian ', ' oslo '], code: 'NO' },
|
| 125 |
+
// Dinamarca
|
| 126 |
+
{ keywords: [' denmark ', ' danish ', ' copenhagen '], code: 'DK' },
|
| 127 |
+
// Finlandia
|
| 128 |
+
{ keywords: [' finland ', ' finnish ', ' helsinki '], code: 'FI' },
|
| 129 |
+
// Grecia
|
| 130 |
+
{ keywords: [' greece ', ' greek ', ' athens '], code: 'GR' },
|
| 131 |
+
// Portugal
|
| 132 |
+
{ keywords: [' portugal ', ' portuguese ', ' lisbon '], code: 'PT' },
|
| 133 |
+
// Bélgica
|
| 134 |
+
{ keywords: [' belgium ', ' belgian ', ' brussels '], code: 'BE' },
|
| 135 |
+
// Austria
|
| 136 |
+
{ keywords: [' austria ', ' austrian ', ' vienna '], code: 'AT' },
|
| 137 |
+
// Irlanda
|
| 138 |
+
{ keywords: [' ireland ', ' irish ', ' dublin '], code: 'IE' },
|
| 139 |
+
// Pakistán
|
| 140 |
+
{ keywords: [' pakistan ', ' pakistani ', ' islamabad '], code: 'PK' },
|
| 141 |
+
// Bangladés
|
| 142 |
+
{ keywords: [' bangladesh ', ' bangladeshi ', ' dhaka '], code: 'BD' },
|
| 143 |
+
// Indonesia
|
| 144 |
+
{ keywords: [' indonesia ', ' indonesian ', ' jakarta '], code: 'ID' },
|
| 145 |
+
// Filipinas
|
| 146 |
+
{ keywords: [' philippines ', ' filipino ', ' manila '], code: 'PH' },
|
| 147 |
+
// Vietnam
|
| 148 |
+
{ keywords: [' vietnam ', ' vietnamese ', ' hanoi '], code: 'VN' },
|
| 149 |
+
// Tailandia
|
| 150 |
+
{ keywords: [' thailand ', ' thai ', ' bangkok '], code: 'TH' },
|
| 151 |
+
// Malasia
|
| 152 |
+
{ keywords: [' malaysia ', ' malaysian ', ' kuala lumpur '], code: 'MY' },
|
| 153 |
+
// Singapur
|
| 154 |
+
{ keywords: [' singapore ', ' singaporean '], code: 'SG' },
|
| 155 |
+
// Colombia
|
| 156 |
+
{ keywords: [' colombia ', ' colombian ', ' bogotá '], code: 'CO' },
|
| 157 |
+
// Chile
|
| 158 |
+
{ keywords: [' chile ', ' chilean ', ' santiago '], code: 'CL' },
|
| 159 |
+
// Perú
|
| 160 |
+
{ keywords: [' peru ', ' peruvian ', ' lima '], code: 'PE' },
|
| 161 |
+
// Venezuela
|
| 162 |
+
{ keywords: [' venezuela ', ' venezuelan ', ' caracas ', ' maduro '], code: 'VE' },
|
| 163 |
+
// Ecuador
|
| 164 |
+
{ keywords: [' ecuador ', ' ecuadorian ', ' quito '], code: 'EC' },
|
| 165 |
+
// Nigeria
|
| 166 |
+
{ keywords: [' nigeria ', ' nigerian ', ' lagos ', ' abuja '], code: 'NG' },
|
| 167 |
+
// Egipto
|
| 168 |
+
{ keywords: [' egypt ', ' egyptian ', ' cairo '], code: 'EG' },
|
| 169 |
+
// Etiopía
|
| 170 |
+
{ keywords: [' ethiopia ', ' ethiopian ', ' addis ababa '], code: 'ET' },
|
| 171 |
+
// Kenia
|
| 172 |
+
{ keywords: [' kenya ', ' kenyan ', ' nairobi '], code: 'KE' },
|
| 173 |
+
// Cuba
|
| 174 |
+
{ keywords: [' cuba ', ' cuban ', ' havana '], code: 'CU' },
|
| 175 |
+
// República Dominicana
|
| 176 |
+
{ keywords: [' dominican republic ', ' dominican '], code: 'DO' },
|
| 177 |
+
// Curazao
|
| 178 |
+
{ keywords: [' curaçao ', ' curacao '], code: 'CW' },
|
| 179 |
+
];
|
| 180 |
+
|
| 181 |
+
for (const rule of rules) {
|
| 182 |
+
if (rule.keywords.some((kw) => text.includes(kw))) {
|
| 183 |
+
return rule.code;
|
| 184 |
+
}
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
return null;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
export function mapMarket(raw) {
|
| 191 |
const { yesPrice, noPrice } = parsePrices(raw.outcomePrices);
|
| 192 |
+
const eventTitle = raw.events?.[0]?.title || '';
|
| 193 |
+
const question = raw.question || '';
|
| 194 |
+
|
| 195 |
return {
|
| 196 |
id: String(raw.id),
|
| 197 |
+
question,
|
| 198 |
+
category: inferCategory(question, eventTitle),
|
| 199 |
+
countryCode: inferCountryCode(question, eventTitle),
|
| 200 |
yesPrice,
|
| 201 |
noPrice,
|
| 202 |
volumeEur: raw.volume != null ? parseFloat(raw.volume) * USD_TO_EUR : null,
|
backend/src/signals/aiPipeline.js
CHANGED
|
@@ -29,7 +29,7 @@ const QWEN_MODEL = 'Qwen/Qwen3-8B';
|
|
| 29 |
const OPENROUTER_API = 'https://openrouter.ai/api/v1/chat/completions';
|
| 30 |
const OPENROUTER_MODEL = 'deepseek/deepseek-chat-v3-0324:free';
|
| 31 |
|
| 32 |
-
//
|
| 33 |
let modernFinBERTClient = null;
|
| 34 |
let qwenClient = null;
|
| 35 |
|
|
@@ -41,7 +41,7 @@ function hfHeaders() {
|
|
| 41 |
|
| 42 |
function extractJson(text) {
|
| 43 |
try { return JSON.parse(text); } catch {}
|
| 44 |
-
//
|
| 45 |
const stripped = text.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
|
| 46 |
try { return JSON.parse(stripped); } catch {}
|
| 47 |
const block = stripped.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
@@ -92,7 +92,7 @@ async function filterWithFinBERTSpace(articles) {
|
|
| 92 |
|
| 93 |
const textBlock = articles.map((a) => a.headline).join('\n');
|
| 94 |
const result = await client.predict('/predict', { text_block: textBlock });
|
| 95 |
-
// Gradio
|
| 96 |
const sentiments = result.data[0];
|
| 97 |
return articles.filter((_, i) => {
|
| 98 |
const r = sentiments[i];
|
|
@@ -110,7 +110,7 @@ async function filterWithFinBERTDirect(articles) {
|
|
| 110 |
{ inputs },
|
| 111 |
{ headers: hfHeaders(), retries: 1 },
|
| 112 |
);
|
| 113 |
-
//
|
| 114 |
return articles.filter((_, i) => {
|
| 115 |
const top = results[i]?.[0];
|
| 116 |
return top && top.label !== 'neutral' && top.score >= 0.65;
|
|
@@ -118,7 +118,7 @@ async function filterWithFinBERTDirect(articles) {
|
|
| 118 |
}
|
| 119 |
|
| 120 |
async function filterWithFinBERT(articles) {
|
| 121 |
-
//
|
| 122 |
if (config.HF_SPACE_MODERNFINBERT_URL) {
|
| 123 |
try {
|
| 124 |
return await filterWithFinBERTSpace(articles);
|
|
@@ -226,7 +226,7 @@ function validateSignal(result) {
|
|
| 226 |
|
| 227 |
function normalizeSignal(result) {
|
| 228 |
if (!result) return result;
|
| 229 |
-
//
|
| 230 |
const signalMap = { buy: 'bullish', sell: 'bearish', hold: 'neutral' };
|
| 231 |
if (result.signal in signalMap) {
|
| 232 |
result.signal = signalMap[result.signal];
|
|
@@ -237,7 +237,7 @@ function normalizeSignal(result) {
|
|
| 237 |
// ── public API ────────────────────────────────────────────────────────────────
|
| 238 |
|
| 239 |
export async function run(market) {
|
| 240 |
-
//
|
| 241 |
let headlines = [];
|
| 242 |
try {
|
| 243 |
const allNews = await fetchFinancialNews(30);
|
|
@@ -251,10 +251,10 @@ export async function run(market) {
|
|
| 251 |
logger.warn({ err: err.message, marketId: market.id }, 'news fetch failed, continuing without news');
|
| 252 |
}
|
| 253 |
|
| 254 |
-
//
|
| 255 |
let result = null;
|
| 256 |
|
| 257 |
-
//
|
| 258 |
if (config.HF_SPACE_QWEN_URL) {
|
| 259 |
try {
|
| 260 |
result = await generateWithQwenSpace(market, headlines);
|
|
@@ -265,7 +265,7 @@ export async function run(market) {
|
|
| 265 |
}
|
| 266 |
}
|
| 267 |
|
| 268 |
-
//
|
| 269 |
if (!result && config.HF_TOKEN) {
|
| 270 |
try {
|
| 271 |
result = await generateWithQwen3Direct(market, headlines);
|
|
@@ -275,7 +275,7 @@ export async function run(market) {
|
|
| 275 |
}
|
| 276 |
}
|
| 277 |
|
| 278 |
-
//
|
| 279 |
if (!result && config.OPENROUTER_API_KEY) {
|
| 280 |
try {
|
| 281 |
result = await generateWithOpenRouter(market, headlines);
|
|
@@ -285,7 +285,7 @@ export async function run(market) {
|
|
| 285 |
}
|
| 286 |
}
|
| 287 |
|
| 288 |
-
//
|
| 289 |
if (!result) {
|
| 290 |
result = ruleBasedSignal(market);
|
| 291 |
}
|
|
|
|
| 29 |
const OPENROUTER_API = 'https://openrouter.ai/api/v1/chat/completions';
|
| 30 |
const OPENROUTER_MODEL = 'deepseek/deepseek-chat-v3-0324:free';
|
| 31 |
|
| 32 |
+
// Clientes Gradio en cache para Spaces
|
| 33 |
let modernFinBERTClient = null;
|
| 34 |
let qwenClient = null;
|
| 35 |
|
|
|
|
| 41 |
|
| 42 |
function extractJson(text) {
|
| 43 |
try { return JSON.parse(text); } catch {}
|
| 44 |
+
// elimina bloques <think>...</think> (modo thinking de Qwen3)
|
| 45 |
const stripped = text.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
|
| 46 |
try { return JSON.parse(stripped); } catch {}
|
| 47 |
const block = stripped.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
|
|
| 92 |
|
| 93 |
const textBlock = articles.map((a) => a.headline).join('\n');
|
| 94 |
const result = await client.predict('/predict', { text_block: textBlock });
|
| 95 |
+
// Gradio devuelve: { data: [ [ {label, score}, ... ] ], ... }
|
| 96 |
const sentiments = result.data[0];
|
| 97 |
return articles.filter((_, i) => {
|
| 98 |
const r = sentiments[i];
|
|
|
|
| 110 |
{ inputs },
|
| 111 |
{ headers: hfHeaders(), retries: 1 },
|
| 112 |
);
|
| 113 |
+
// conserva titulares clasificados como positivos o negativos con score >= 0.65
|
| 114 |
return articles.filter((_, i) => {
|
| 115 |
const top = results[i]?.[0];
|
| 116 |
return top && top.label !== 'neutral' && top.score >= 0.65;
|
|
|
|
| 118 |
}
|
| 119 |
|
| 120 |
async function filterWithFinBERT(articles) {
|
| 121 |
+
// Prefiere Space si esta configurado, respaldo a API directa
|
| 122 |
if (config.HF_SPACE_MODERNFINBERT_URL) {
|
| 123 |
try {
|
| 124 |
return await filterWithFinBERTSpace(articles);
|
|
|
|
| 226 |
|
| 227 |
function normalizeSignal(result) {
|
| 228 |
if (!result) return result;
|
| 229 |
+
// Normaliza buy/sell/hold a bullish/bearish/neutral para compatibilidad descendente
|
| 230 |
const signalMap = { buy: 'bullish', sell: 'bearish', hold: 'neutral' };
|
| 231 |
if (result.signal in signalMap) {
|
| 232 |
result.signal = signalMap[result.signal];
|
|
|
|
| 237 |
// ── public API ────────────────────────────────────────────────────────────────
|
| 238 |
|
| 239 |
export async function run(market) {
|
| 240 |
+
// Paso 1: obtiene y filtra noticias
|
| 241 |
let headlines = [];
|
| 242 |
try {
|
| 243 |
const allNews = await fetchFinancialNews(30);
|
|
|
|
| 251 |
logger.warn({ err: err.message, marketId: market.id }, 'news fetch failed, continuing without news');
|
| 252 |
}
|
| 253 |
|
| 254 |
+
// Paso 2: generacion de senal LLM con cadena de respaldo
|
| 255 |
let result = null;
|
| 256 |
|
| 257 |
+
// Intenta Space primero
|
| 258 |
if (config.HF_SPACE_QWEN_URL) {
|
| 259 |
try {
|
| 260 |
result = await generateWithQwenSpace(market, headlines);
|
|
|
|
| 265 |
}
|
| 266 |
}
|
| 267 |
|
| 268 |
+
// Respaldo a API directa de HF
|
| 269 |
if (!result && config.HF_TOKEN) {
|
| 270 |
try {
|
| 271 |
result = await generateWithQwen3Direct(market, headlines);
|
|
|
|
| 275 |
}
|
| 276 |
}
|
| 277 |
|
| 278 |
+
// Respaldo a OpenRouter
|
| 279 |
if (!result && config.OPENROUTER_API_KEY) {
|
| 280 |
try {
|
| 281 |
result = await generateWithOpenRouter(market, headlines);
|
|
|
|
| 285 |
}
|
| 286 |
}
|
| 287 |
|
| 288 |
+
// Respaldo final
|
| 289 |
if (!result) {
|
| 290 |
result = ruleBasedSignal(market);
|
| 291 |
}
|
backend/src/signals/signals.controller.js
CHANGED
|
@@ -20,4 +20,11 @@ export const signalsController = {
|
|
| 20 |
const signal = await signalsService.getLatest(req.params.marketId);
|
| 21 |
ok(res, signal);
|
| 22 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
};
|
|
|
|
| 20 |
const signal = await signalsService.getLatest(req.params.marketId);
|
| 21 |
ok(res, signal);
|
| 22 |
},
|
| 23 |
+
|
| 24 |
+
async getLatestBatch(req, res) {
|
| 25 |
+
const ids = req.query.marketIds;
|
| 26 |
+
const marketIds = ids ? ids.split(',').filter(Boolean) : [];
|
| 27 |
+
const signals = await signalsService.getLatestBatch(marketIds);
|
| 28 |
+
ok(res, signals);
|
| 29 |
+
},
|
| 30 |
};
|
backend/src/signals/signals.repository.js
CHANGED
|
@@ -26,4 +26,20 @@ export const signalsRepository = {
|
|
| 26 |
orderBy: { generatedAt: 'desc' },
|
| 27 |
});
|
| 28 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
};
|
|
|
|
| 26 |
orderBy: { generatedAt: 'desc' },
|
| 27 |
});
|
| 28 |
},
|
| 29 |
+
|
| 30 |
+
findLatestForMarkets(marketIds) {
|
| 31 |
+
if (!marketIds || marketIds.length === 0) return Promise.resolve([]);
|
| 32 |
+
return prisma.aISignal.findMany({
|
| 33 |
+
where: { marketId: { in: marketIds } },
|
| 34 |
+
orderBy: [{ marketId: 'asc' }, { generatedAt: 'desc' }],
|
| 35 |
+
}).then((rows) => {
|
| 36 |
+
// Tomar solo la más reciente por marketId
|
| 37 |
+
const seen = new Set();
|
| 38 |
+
return rows.filter((r) => {
|
| 39 |
+
if (seen.has(r.marketId)) return false;
|
| 40 |
+
seen.add(r.marketId);
|
| 41 |
+
return true;
|
| 42 |
+
});
|
| 43 |
+
});
|
| 44 |
+
},
|
| 45 |
};
|
backend/src/signals/signals.routes.js
CHANGED
|
@@ -12,6 +12,10 @@ import { signalsController } from './signals.controller.js';
|
|
| 12 |
|
| 13 |
const router = Router();
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
// mounted at /api/v1/markets → full path: GET /api/v1/markets/:marketId/signal
|
| 16 |
router.get('/:marketId/signal', signalsController.getLatest);
|
| 17 |
|
|
|
|
| 12 |
|
| 13 |
const router = Router();
|
| 14 |
|
| 15 |
+
// Batch: últimas señales para múltiples mercados (debe ir ANTES de la ruta dinámica)
|
| 16 |
+
// mounted at /api/v1/markets → full path: GET /api/v1/markets/signals/latest?marketIds=id1,id2
|
| 17 |
+
router.get('/signals/latest', signalsController.getLatestBatch);
|
| 18 |
+
|
| 19 |
// mounted at /api/v1/markets → full path: GET /api/v1/markets/:marketId/signal
|
| 20 |
router.get('/:marketId/signal', signalsController.getLatest);
|
| 21 |
|
backend/src/signals/signals.service.js
CHANGED
|
@@ -32,6 +32,12 @@ export const signalsService = {
|
|
| 32 |
return signal;
|
| 33 |
},
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
async generateForMarket(market) {
|
| 36 |
const modelVersion = 'Qwen3-8B';
|
| 37 |
const result = await aiPipeline.run(market);
|
|
|
|
| 32 |
return signal;
|
| 33 |
},
|
| 34 |
|
| 35 |
+
async getLatestBatch(marketIds) {
|
| 36 |
+
if (!marketIds || marketIds.length === 0) return [];
|
| 37 |
+
const signals = await signalsRepository.findLatestForMarkets(marketIds);
|
| 38 |
+
return signals;
|
| 39 |
+
},
|
| 40 |
+
|
| 41 |
async generateForMarket(market) {
|
| 42 |
const modelVersion = 'Qwen3-8B';
|
| 43 |
const result = await aiPipeline.run(market);
|
backend/src/stats/stats.routes.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Rutas REST de estadisticas globales (publicas).
|
| 3 |
+
*
|
| 4 |
+
* Endpoint (montado en /api/v1/stats):
|
| 5 |
+
* GET / → estadisticas agregadas del sistema.
|
| 6 |
+
*
|
| 7 |
+
* Datos expuestos:
|
| 8 |
+
* - marketsCount : total de mercados activos.
|
| 9 |
+
* - volume24h : suma de volumen de mercados activos.
|
| 10 |
+
* - signalsCount : total de senales IA generadas.
|
| 11 |
+
* - alertsToday : alertas enviadas en las ultimas 24h.
|
| 12 |
+
*/
|
| 13 |
+
|
| 14 |
+
import { Router } from 'express';
|
| 15 |
+
import { prisma } from '../utils/prisma.js';
|
| 16 |
+
import { ok } from '../utils/apiResponse.js';
|
| 17 |
+
|
| 18 |
+
const router = Router();
|
| 19 |
+
|
| 20 |
+
router.get('/', async (_req, res) => {
|
| 21 |
+
const [marketsCount, signalsCount, alertsToday] = await Promise.all([
|
| 22 |
+
prisma.market.count({ where: { status: 'active' } }),
|
| 23 |
+
prisma.aISignal.count(),
|
| 24 |
+
prisma.alert.count({
|
| 25 |
+
where: { sentAt: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) } },
|
| 26 |
+
}),
|
| 27 |
+
]);
|
| 28 |
+
|
| 29 |
+
const volumeAgg = await prisma.market.aggregate({
|
| 30 |
+
where: { status: 'active' },
|
| 31 |
+
_sum: { volumeEur: true },
|
| 32 |
+
});
|
| 33 |
+
|
| 34 |
+
ok(res, {
|
| 35 |
+
marketsCount,
|
| 36 |
+
volume24h: volumeAgg._sum.volumeEur || 0,
|
| 37 |
+
signalsCount,
|
| 38 |
+
alertsToday,
|
| 39 |
+
});
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
export default router;
|
frontend/dist/assets/index-B7Y-b3uS.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/dist/assets/index-DUFVKRJi.js
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/dist/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
<title>PolySignal — Dashboard de Inteligencia de Mercados</title>
|
| 7 |
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 8 |
-
<script type="module" crossorigin src="/assets/index-
|
| 9 |
<link rel="stylesheet" crossorigin href="/assets/index-xtYPhhTl.css">
|
| 10 |
</head>
|
| 11 |
<body>
|
|
|
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
<title>PolySignal — Dashboard de Inteligencia de Mercados</title>
|
| 7 |
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 8 |
+
<script type="module" crossorigin src="/assets/index-B7Y-b3uS.js"></script>
|
| 9 |
<link rel="stylesheet" crossorigin href="/assets/index-xtYPhhTl.css">
|
| 10 |
</head>
|
| 11 |
<body>
|
frontend/src/api.js
CHANGED
|
@@ -3,10 +3,13 @@
|
|
| 3 |
*
|
| 4 |
* Responsabilidades:
|
| 5 |
* - Realizar peticiones fetch a /api/v1/* con headers JSON.
|
|
|
|
|
|
|
| 6 |
* - Gestionar errores HTTP (lanza Error con status y body).
|
| 7 |
* - Manejar 204 No Content (retorna null).
|
| 8 |
*
|
| 9 |
* Modulos cubiertos:
|
|
|
|
| 10 |
* - Markets → getMarkets(params), getMarket(id)
|
| 11 |
* - Signals → getSignal(marketId)
|
| 12 |
* - Positions → getPositions(), createPosition(data), closePosition(id)
|
|
@@ -18,18 +21,71 @@
|
|
| 18 |
*/
|
| 19 |
|
| 20 |
const BASE = '/api/v1'
|
|
|
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
async function fetchJson(url, opts = {}) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
const res = await fetch(url, {
|
| 24 |
-
headers
|
| 25 |
...opts,
|
| 26 |
})
|
|
|
|
| 27 |
if (!res.ok) {
|
| 28 |
const text = await res.text().catch(() => '')
|
| 29 |
throw new Error(`HTTP ${res.status}: ${text}`)
|
| 30 |
}
|
|
|
|
| 31 |
if (res.status === 204) return null
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
}
|
| 34 |
|
| 35 |
/* ─── Markets ─── */
|
|
@@ -47,6 +103,12 @@ export async function getSignal(marketId) {
|
|
| 47 |
return fetchJson(`${BASE}/markets/${marketId}/signal`)
|
| 48 |
}
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
/* ─── Positions ─── */
|
| 51 |
export async function getPositions() {
|
| 52 |
return fetchJson(`${BASE}/positions`)
|
|
|
|
| 3 |
*
|
| 4 |
* Responsabilidades:
|
| 5 |
* - Realizar peticiones fetch a /api/v1/* con headers JSON.
|
| 6 |
+
* - Gestionar token JWT (login, logout, Authorization header).
|
| 7 |
+
* - Extraer el campo `data` de las respuestas estandarizadas del backend.
|
| 8 |
* - Gestionar errores HTTP (lanza Error con status y body).
|
| 9 |
* - Manejar 204 No Content (retorna null).
|
| 10 |
*
|
| 11 |
* Modulos cubiertos:
|
| 12 |
+
* - Auth → login(email, password), logout()
|
| 13 |
* - Markets → getMarkets(params), getMarket(id)
|
| 14 |
* - Signals → getSignal(marketId)
|
| 15 |
* - Positions → getPositions(), createPosition(data), closePosition(id)
|
|
|
|
| 21 |
*/
|
| 22 |
|
| 23 |
const BASE = '/api/v1'
|
| 24 |
+
const TOKEN_KEY = 'polysignal_token'
|
| 25 |
|
| 26 |
+
/* ─── Token helpers ─── */
|
| 27 |
+
export function getToken() {
|
| 28 |
+
return localStorage.getItem(TOKEN_KEY)
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
export function setToken(token) {
|
| 32 |
+
localStorage.setItem(TOKEN_KEY, token)
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
export function clearToken() {
|
| 36 |
+
localStorage.removeItem(TOKEN_KEY)
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
export function isAuthenticated() {
|
| 40 |
+
return !!getToken()
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/* ─── Auth ─── */
|
| 44 |
+
export async function login(email, password) {
|
| 45 |
+
const body = await fetchJson(`${BASE}/auth/login`, {
|
| 46 |
+
method: 'POST',
|
| 47 |
+
body: JSON.stringify({ email, password }),
|
| 48 |
+
skipAuth: true,
|
| 49 |
+
})
|
| 50 |
+
if (body.token) {
|
| 51 |
+
setToken(body.token)
|
| 52 |
+
}
|
| 53 |
+
return body
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
export function logout() {
|
| 57 |
+
clearToken()
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/* ─── Core fetch ─── */
|
| 61 |
async function fetchJson(url, opts = {}) {
|
| 62 |
+
const headers = { 'Content-Type': 'application/json', ...opts.headers }
|
| 63 |
+
|
| 64 |
+
if (!opts.skipAuth) {
|
| 65 |
+
const token = getToken()
|
| 66 |
+
if (token) {
|
| 67 |
+
headers.Authorization = `Bearer ${token}`
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
const res = await fetch(url, {
|
| 72 |
+
headers,
|
| 73 |
...opts,
|
| 74 |
})
|
| 75 |
+
|
| 76 |
if (!res.ok) {
|
| 77 |
const text = await res.text().catch(() => '')
|
| 78 |
throw new Error(`HTTP ${res.status}: ${text}`)
|
| 79 |
}
|
| 80 |
+
|
| 81 |
if (res.status === 204) return null
|
| 82 |
+
|
| 83 |
+
const body = await res.json()
|
| 84 |
+
// El backend envuelve respuestas exitosas en { ok: true, data: ... }
|
| 85 |
+
if (body && body.ok === true && 'data' in body) {
|
| 86 |
+
return body.data
|
| 87 |
+
}
|
| 88 |
+
return body
|
| 89 |
}
|
| 90 |
|
| 91 |
/* ─── Markets ─── */
|
|
|
|
| 103 |
return fetchJson(`${BASE}/markets/${marketId}/signal`)
|
| 104 |
}
|
| 105 |
|
| 106 |
+
export async function getSignalsBatch(marketIds) {
|
| 107 |
+
if (!marketIds || marketIds.length === 0) return []
|
| 108 |
+
const qs = new URLSearchParams({ marketIds: marketIds.join(',') }).toString()
|
| 109 |
+
return fetchJson(`${BASE}/markets/signals/latest?${qs}`)
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
/* ─── Positions ─── */
|
| 113 |
export async function getPositions() {
|
| 114 |
return fetchJson(`${BASE}/positions`)
|
frontend/src/app.js
CHANGED
|
@@ -11,10 +11,10 @@
|
|
| 11 |
* * price_alert → anade alertas al historial.
|
| 12 |
* - Renderizado del panel de detalle de mercado con sparklines, grafico 7d,
|
| 13 |
* analisis IA y simulador de posiciones virtuales.
|
| 14 |
-
* -
|
| 15 |
*
|
| 16 |
* Modulos importados:
|
| 17 |
-
* - api.js → cliente REST del backend.
|
| 18 |
* - charts.js → Chart.js (historial 7d + sparklines).
|
| 19 |
* - map.js → Leaflet (mapa mundial interactivo).
|
| 20 |
* - simulator.js → logica de compra/venta virtual.
|
|
@@ -44,175 +44,20 @@ let state = {
|
|
| 44 |
sidebarCollapsed: false,
|
| 45 |
}
|
| 46 |
|
| 47 |
-
/* ─── Datos mock para demo (fallback si backend no responde) ─── */
|
| 48 |
-
const MOCK_MARKETS = [
|
| 49 |
-
{
|
| 50 |
-
id: 'usa-001',
|
| 51 |
-
question: '¿Trump firmará la ley fiscal antes de junio de 2026?',
|
| 52 |
-
category: 'política',
|
| 53 |
-
countryCode: 'US',
|
| 54 |
-
yesPrice: 0.73,
|
| 55 |
-
noPrice: 0.27,
|
| 56 |
-
volumeEur: 1240000,
|
| 57 |
-
liquidityEur: 340000,
|
| 58 |
-
status: 'active',
|
| 59 |
-
closesAt: '2026-06-01T00:00:00Z',
|
| 60 |
-
},
|
| 61 |
-
{
|
| 62 |
-
id: 'eur-001',
|
| 63 |
-
question: '¿El BCE recortará tipos en junio de 2026?',
|
| 64 |
-
category: 'economía',
|
| 65 |
-
countryCode: 'DE',
|
| 66 |
-
yesPrice: 0.34,
|
| 67 |
-
noPrice: 0.66,
|
| 68 |
-
volumeEur: 890000,
|
| 69 |
-
liquidityEur: 210000,
|
| 70 |
-
status: 'active',
|
| 71 |
-
closesAt: '2026-06-12T00:00:00Z',
|
| 72 |
-
},
|
| 73 |
-
{
|
| 74 |
-
id: 'chn-001',
|
| 75 |
-
question: '¿Se alcanzará un acuerdo arancelario EE.UU.-China antes del T3 2026?',
|
| 76 |
-
category: 'comercio',
|
| 77 |
-
countryCode: 'CN',
|
| 78 |
-
yesPrice: 0.51,
|
| 79 |
-
noPrice: 0.49,
|
| 80 |
-
volumeEur: 2100000,
|
| 81 |
-
liquidityEur: 580000,
|
| 82 |
-
status: 'active',
|
| 83 |
-
closesAt: '2026-07-01T00:00:00Z',
|
| 84 |
-
},
|
| 85 |
-
{
|
| 86 |
-
id: 'bra-001',
|
| 87 |
-
question: '¿Bitcoin superará los $120.000 antes de julio de 2026?',
|
| 88 |
-
category: 'cripto',
|
| 89 |
-
countryCode: 'BR',
|
| 90 |
-
yesPrice: 0.61,
|
| 91 |
-
noPrice: 0.39,
|
| 92 |
-
volumeEur: 3400000,
|
| 93 |
-
liquidityEur: 920000,
|
| 94 |
-
status: 'active',
|
| 95 |
-
closesAt: '2026-07-01T00:00:00Z',
|
| 96 |
-
},
|
| 97 |
-
{
|
| 98 |
-
id: 'uk-001',
|
| 99 |
-
question: '¿La inflación del Reino Unido bajará del 2% en 2026?',
|
| 100 |
-
category: 'economía',
|
| 101 |
-
countryCode: 'GB',
|
| 102 |
-
yesPrice: 0.45,
|
| 103 |
-
noPrice: 0.55,
|
| 104 |
-
volumeEur: 620000,
|
| 105 |
-
liquidityEur: 180000,
|
| 106 |
-
status: 'active',
|
| 107 |
-
closesAt: '2026-09-01T00:00:00Z',
|
| 108 |
-
},
|
| 109 |
-
{
|
| 110 |
-
id: 'ind-001',
|
| 111 |
-
question: '¿El crecimiento del PIB de India superará el 7% en el AF2026?',
|
| 112 |
-
category: 'economía',
|
| 113 |
-
countryCode: 'IN',
|
| 114 |
-
yesPrice: 0.68,
|
| 115 |
-
noPrice: 0.32,
|
| 116 |
-
volumeEur: 450000,
|
| 117 |
-
liquidityEur: 120000,
|
| 118 |
-
status: 'active',
|
| 119 |
-
closesAt: '2026-03-31T00:00:00Z',
|
| 120 |
-
},
|
| 121 |
-
]
|
| 122 |
-
|
| 123 |
-
const MOCK_SIGNALS = {
|
| 124 |
-
'usa-001': {
|
| 125 |
-
signal: 'bullish',
|
| 126 |
-
confidence: 0.87,
|
| 127 |
-
summary: 'El liderazgo republicano confirmó 3 de 4 votos de comité asegurados. El sentimiento de noticias de 8 fuentes es positivo (+0,72). La línea temporal del Congreso sugiere que la ventana de firma se abre del 28 al 31 de mayo.',
|
| 128 |
-
keyRisk: 'El proceso de enmiendas en el Senado podría retrasar más allá del 1 de junio.',
|
| 129 |
-
},
|
| 130 |
-
'eur-001': {
|
| 131 |
-
signal: 'bearish',
|
| 132 |
-
confidence: 0.74,
|
| 133 |
-
summary: 'La inflación de la zona euro sorprendió al alza al 2,4% en abril. Las recientes declaraciones de los miembros del consejo de gobierno del BCE señalan que es más probable una pausa.',
|
| 134 |
-
keyRisk: 'La valoración del mercado de bonos implica solo un 22% de probabilidad de recorte en junio.',
|
| 135 |
-
},
|
| 136 |
-
'chn-001': {
|
| 137 |
-
signal: 'neutral',
|
| 138 |
-
confidence: 0.52,
|
| 139 |
-
summary: 'Las negociaciones continúan pero no se ha acordado un marco formal. Los flujos comerciales muestran patrones tempranos de evasión arancelaria, sugiriendo que ambas partes ganan tiempo.',
|
| 140 |
-
keyRisk: 'Demasiadas variables geopolíticas. Se monitorea la declaración del USTR como señal clave.',
|
| 141 |
-
},
|
| 142 |
-
'bra-001': {
|
| 143 |
-
signal: 'bullish',
|
| 144 |
-
confidence: 0.79,
|
| 145 |
-
summary: 'BTC actualmente en $103.400. Las métricas on-chain muestran acumulación de grandes carteras. Entradas en ETF +$820M esta semana. El mercado de opciones implica 68% de probabilidad de $120K a finales de junio.',
|
| 146 |
-
keyRisk: 'Las condiciones de liquidez macro podrían cambiar si la Fed mantiene una postura hawkish.',
|
| 147 |
-
},
|
| 148 |
-
'uk-001': {
|
| 149 |
-
signal: 'neutral',
|
| 150 |
-
confidence: 0.58,
|
| 151 |
-
summary: 'Las previsiones del BoE sugieren una caída gradual pero los shocks externos de precios de energía siguen siendo una incógnita. La inflación de servicios se mantiene en el 5,2%.',
|
| 152 |
-
keyRisk: 'Las interrupciones geopolíticas en la cadena de suministro podrían reavivar la inflación de bienes.',
|
| 153 |
-
},
|
| 154 |
-
'ind-001': {
|
| 155 |
-
signal: 'bullish',
|
| 156 |
-
confidence: 0.71,
|
| 157 |
-
summary: 'Dato del T4 AF25 en 7,1% con expansión del PMI manufacturero. El impulso del gasto público en infraestructura probablemente sostenga el momentum en el S1 AF26.',
|
| 158 |
-
keyRisk: 'El impacto de El Niño en la producción agrícola podría reducir 30-40 pb del dato principal.',
|
| 159 |
-
},
|
| 160 |
-
}
|
| 161 |
-
|
| 162 |
-
const MOCK_POSITIONS = [
|
| 163 |
-
{
|
| 164 |
-
id: 1,
|
| 165 |
-
marketId: 'usa-001',
|
| 166 |
-
outcome: 'SÍ',
|
| 167 |
-
amountEur: 100,
|
| 168 |
-
entryPrice: 0.68,
|
| 169 |
-
currentPrice: 0.73,
|
| 170 |
-
pnl: 14.20,
|
| 171 |
-
kellyFraction: 0.25,
|
| 172 |
-
openedAt: '2026-05-10T14:30:00Z',
|
| 173 |
-
},
|
| 174 |
-
{
|
| 175 |
-
id: 2,
|
| 176 |
-
marketId: 'eur-001',
|
| 177 |
-
outcome: 'NO',
|
| 178 |
-
amountEur: 80,
|
| 179 |
-
entryPrice: 0.62,
|
| 180 |
-
currentPrice: 0.66,
|
| 181 |
-
pnl: -3.80,
|
| 182 |
-
kellyFraction: 0.18,
|
| 183 |
-
openedAt: '2026-05-11T09:15:00Z',
|
| 184 |
-
},
|
| 185 |
-
]
|
| 186 |
-
|
| 187 |
-
const MOCK_ALERTS = [
|
| 188 |
-
{
|
| 189 |
-
id: 1,
|
| 190 |
-
marketId: 'usa-001',
|
| 191 |
-
type: 'cambio_señal',
|
| 192 |
-
message: 'La señal cambió a ALCISTA (87%) en ley fiscal de Trump',
|
| 193 |
-
sentAt: '2026-05-14T08:23:00Z',
|
| 194 |
-
},
|
| 195 |
-
{
|
| 196 |
-
id: 2,
|
| 197 |
-
marketId: 'bra-001',
|
| 198 |
-
type: 'umbral_precio',
|
| 199 |
-
message: 'El mercado BTC $120K SÍ cruzó el umbral de 60¢',
|
| 200 |
-
sentAt: '2026-05-14T07:45:00Z',
|
| 201 |
-
},
|
| 202 |
-
]
|
| 203 |
-
|
| 204 |
/* ─── Helpers ─── */
|
| 205 |
function formatCurrency(n) {
|
|
|
|
| 206 |
if (n >= 1e6) return '€' + (n / 1e6).toFixed(1) + 'M'
|
| 207 |
if (n >= 1e3) return '€' + (n / 1e3).toFixed(1) + 'K'
|
| 208 |
return '€' + n.toFixed(0)
|
| 209 |
}
|
| 210 |
|
| 211 |
function formatPrice(p) {
|
| 212 |
-
return Math.round(p * 100) + '¢'
|
| 213 |
}
|
| 214 |
|
| 215 |
function formatDate(iso) {
|
|
|
|
| 216 |
const d = new Date(iso)
|
| 217 |
return d.toLocaleDateString('es-ES', { month: 'short', day: 'numeric', year: 'numeric' })
|
| 218 |
}
|
|
@@ -241,7 +86,13 @@ function translateSignal(signal) {
|
|
| 241 |
return 'neutral'
|
| 242 |
}
|
| 243 |
|
| 244 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
function el(tag, className, text) {
|
| 246 |
const node = document.createElement(tag)
|
| 247 |
if (className) node.className = className
|
|
@@ -253,6 +104,18 @@ function emptyState(text, sm = false) {
|
|
| 253 |
return el('div', sm ? 'empty-state empty-state-sm' : 'empty-state', text)
|
| 254 |
}
|
| 255 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
/* ─── Routing de vistas ─── */
|
| 257 |
function switchView(viewName) {
|
| 258 |
state.view = viewName
|
|
@@ -284,8 +147,13 @@ function renderSignals() {
|
|
| 284 |
if (!container) return
|
| 285 |
container.replaceChildren()
|
| 286 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
state.markets.forEach((m) => {
|
| 288 |
-
const sig = state.signals.find((s) => s.marketId === m.id) ||
|
| 289 |
const cls = signalColorClass(sig.signal)
|
| 290 |
|
| 291 |
const card = el('div', `market-card${state.activeMarketId === m.id ? ' active' : ''}`)
|
|
@@ -302,7 +170,7 @@ function renderSignals() {
|
|
| 302 |
const probWrap = el('div', 'prob-bar-wrap')
|
| 303 |
const probBg = el('div', 'prob-bar-bg')
|
| 304 |
const probFill = el('div', `prob-bar-fill bg-${cls}`)
|
| 305 |
-
probFill.style.setProperty('--prob-width', `${Math.round(m.yesPrice * 100)}%`)
|
| 306 |
probBg.appendChild(probFill)
|
| 307 |
probWrap.appendChild(probBg)
|
| 308 |
|
|
@@ -331,16 +199,16 @@ function renderMiniPositions() {
|
|
| 331 |
|
| 332 |
let netPnl = 0
|
| 333 |
state.positions.forEach((p) => {
|
| 334 |
-
const m = state.markets.find((x) => x.id === p.marketId) || { question: p.marketId }
|
| 335 |
-
netPnl += p.pnl
|
| 336 |
-
const cls = p.pnl >= 0 ? 'green' : 'red'
|
| 337 |
-
const sign = p.pnl >= 0 ? '+' : ''
|
| 338 |
|
| 339 |
const row = el('div', 'flex-between mb-6')
|
| 340 |
const label = el('span', 'text-sm text-neutral font-mono')
|
| 341 |
-
label.textContent = `${m.question.substring(0, 32)}${m.question.length > 32 ? '…' : ''} ${p.outcome}`
|
| 342 |
const val = el('span', `text-base font-semibold text-${cls} font-mono`)
|
| 343 |
-
val.textContent = `${sign}€${p.pnl.toFixed(2)}`
|
| 344 |
row.append(label, val)
|
| 345 |
container.appendChild(row)
|
| 346 |
})
|
|
@@ -365,10 +233,15 @@ function renderDetail() {
|
|
| 365 |
return
|
| 366 |
}
|
| 367 |
|
| 368 |
-
const sig = state.signals.find((s) => s.marketId === m.id) ||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
const delta = ((m.yesPrice - 0.5) * 20).toFixed(1)
|
| 370 |
-
const deltaCls = m.yesPrice > 0.5 ? 'green' : 'red'
|
| 371 |
-
const deltaSign = m.yesPrice > 0.5 ? '+' : ''
|
| 372 |
|
| 373 |
// ── Header ──
|
| 374 |
const tag = el('div', 'detail-tag')
|
|
@@ -402,7 +275,7 @@ function renderDetail() {
|
|
| 402 |
const yesPriceEl = el('div', 'outcome-price')
|
| 403 |
yesPriceEl.textContent = formatPrice(m.yesPrice)
|
| 404 |
const yesDeltaEl = el('div', 'outcome-delta td-green')
|
| 405 |
-
yesDeltaEl.textContent = `▲ ${(m.yesPrice * 0.05).toFixed(1)}¢`
|
| 406 |
const yesCard = el('div', 'outcome-card yes')
|
| 407 |
yesCard.append(el('div', 'outcome-name', 'SÍ'), yesPriceEl, yesDeltaEl, sparkYes)
|
| 408 |
|
|
@@ -411,7 +284,7 @@ function renderDetail() {
|
|
| 411 |
const noPriceEl = el('div', 'outcome-price')
|
| 412 |
noPriceEl.textContent = formatPrice(m.noPrice)
|
| 413 |
const noDeltaEl = el('div', 'outcome-delta td-red')
|
| 414 |
-
noDeltaEl.textContent = `▼ ${(m.noPrice * 0.05).toFixed(1)}¢`
|
| 415 |
const noCard = el('div', 'outcome-card no')
|
| 416 |
noCard.append(el('div', 'outcome-name', 'NO'), noPriceEl, noDeltaEl, sparkNo)
|
| 417 |
|
|
@@ -430,9 +303,9 @@ function renderDetail() {
|
|
| 430 |
const aiHeader = el('div', 'flex-row gap-8 mb-4 flex-wrap')
|
| 431 |
aiHeader.append(el('div', 'ai-label', 'Análisis IA · HuggingFace Qwen3-8B'), aiBadge, aiMeta)
|
| 432 |
|
| 433 |
-
//
|
| 434 |
const aiText = el('div', 'ai-text')
|
| 435 |
-
aiText.textContent = sig.summary
|
| 436 |
if (sig.keyRisk) {
|
| 437 |
const strong = document.createElement('strong')
|
| 438 |
strong.textContent = 'Riesgo clave:'
|
|
@@ -468,11 +341,11 @@ function renderDetail() {
|
|
| 468 |
|
| 469 |
container.replaceChildren(header, outcomesRow, aiBox, simRow)
|
| 470 |
|
| 471 |
-
//
|
| 472 |
-
simYes.addEventListener('click', () => simulator.openPosition(m.id, '
|
| 473 |
simNo.addEventListener('click', () => simulator.openPosition(m.id, 'NO', simAmount.value))
|
| 474 |
|
| 475 |
-
//
|
| 476 |
charts.renderDetailChart('detail-chart', m.yesPrice)
|
| 477 |
charts.renderSparkline('spark-yes', m.yesPrice, 'yes')
|
| 478 |
charts.renderSparkline('spark-no', m.noPrice, 'no')
|
|
@@ -500,17 +373,17 @@ function renderPositions() {
|
|
| 500 |
tbody.replaceChildren()
|
| 501 |
|
| 502 |
state.positions.forEach((p) => {
|
| 503 |
-
const m = state.markets.find((x) => x.id === p.marketId) || { question: p.marketId }
|
| 504 |
-
const pnlColor = p.pnl >= 0 ? 'td-green' : 'td-red'
|
| 505 |
-
const sign = p.pnl >= 0 ? '+' : ''
|
| 506 |
|
| 507 |
const tr = document.createElement('tr')
|
| 508 |
|
| 509 |
const tdQ = el('td')
|
| 510 |
-
tdQ.textContent = `${m.question.substring(0, 40)}${m.question.length > 40 ? '…' : ''}`
|
| 511 |
|
| 512 |
-
const tdOutcome = el('td', `td-mono ${p.outcome === '
|
| 513 |
-
tdOutcome.textContent = p.outcome
|
| 514 |
|
| 515 |
const tdAmt = el('td', 'td-mono')
|
| 516 |
tdAmt.textContent = `€${p.amountEur.toFixed(0)}`
|
|
@@ -522,7 +395,7 @@ function renderPositions() {
|
|
| 522 |
tdCurrent.textContent = formatPrice(p.currentPrice)
|
| 523 |
|
| 524 |
const tdPnl = el('td', `td-mono ${pnlColor}`)
|
| 525 |
-
tdPnl.textContent = `${sign}€${p.pnl.toFixed(2)}`
|
| 526 |
|
| 527 |
const tdKelly = el('td', 'td-mono td-blue')
|
| 528 |
tdKelly.textContent = `${((p.kellyFraction || 0) * 100).toFixed(0)}%`
|
|
@@ -561,13 +434,13 @@ function renderWatchlist() {
|
|
| 561 |
tbody.replaceChildren()
|
| 562 |
|
| 563 |
state.watchlist.forEach((w) => {
|
| 564 |
-
const m = state.markets.find((x) => x.id === w.marketId) || { question: w.marketId, category: '-', yesPrice: 0, noPrice: 0, volumeEur: 0 }
|
| 565 |
const sig = state.signals.find((s) => s.marketId === w.marketId) || { signal: 'neutral' }
|
| 566 |
|
| 567 |
const tr = document.createElement('tr')
|
| 568 |
|
| 569 |
const tdQ = el('td')
|
| 570 |
-
tdQ.textContent = `${m.question.substring(0, 40)}${m.question.length > 40 ? '…' : ''}`
|
| 571 |
|
| 572 |
const tdCat = el('td')
|
| 573 |
tdCat.textContent = m.category || '-'
|
|
@@ -619,7 +492,7 @@ function renderAlerts() {
|
|
| 619 |
tbody.replaceChildren()
|
| 620 |
|
| 621 |
state.alerts.forEach((a) => {
|
| 622 |
-
const m = state.markets.find((x) => x.id === a.marketId) || { question: a.marketId }
|
| 623 |
|
| 624 |
const tr = document.createElement('tr')
|
| 625 |
|
|
@@ -627,7 +500,7 @@ function renderAlerts() {
|
|
| 627 |
tdDate.textContent = new Date(a.sentAt).toLocaleString('es-ES')
|
| 628 |
|
| 629 |
const tdQ = el('td')
|
| 630 |
-
tdQ.textContent = `${m.question.substring(0, 35)}${m.question.length > 35 ? '…' : ''}`
|
| 631 |
|
| 632 |
const typeBadge = el('span', 'signal-badge sig-neut')
|
| 633 |
typeBadge.textContent = a.type
|
|
@@ -645,22 +518,25 @@ function renderAlerts() {
|
|
| 645 |
/* ─── Carga de datos ─── */
|
| 646 |
async function loadMarkets() {
|
| 647 |
try {
|
| 648 |
-
|
| 649 |
-
state.markets = data.length ? data : MOCK_MARKETS
|
| 650 |
} catch (e) {
|
| 651 |
-
console.
|
| 652 |
-
state.markets =
|
| 653 |
}
|
| 654 |
}
|
| 655 |
|
| 656 |
async function loadSignals() {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 657 |
try {
|
| 658 |
-
const
|
| 659 |
-
const results = await
|
| 660 |
-
state.signals = results.
|
| 661 |
} catch (e) {
|
| 662 |
-
console.
|
| 663 |
-
state.signals =
|
| 664 |
}
|
| 665 |
}
|
| 666 |
|
|
@@ -668,7 +544,8 @@ async function loadPositions() {
|
|
| 668 |
try {
|
| 669 |
state.positions = await api.getPositions()
|
| 670 |
} catch (e) {
|
| 671 |
-
|
|
|
|
| 672 |
}
|
| 673 |
}
|
| 674 |
|
|
@@ -676,6 +553,7 @@ async function loadWatchlist() {
|
|
| 676 |
try {
|
| 677 |
state.watchlist = await api.getWatchlist()
|
| 678 |
} catch (e) {
|
|
|
|
| 679 |
state.watchlist = []
|
| 680 |
}
|
| 681 |
}
|
|
@@ -684,7 +562,24 @@ async function loadAlerts() {
|
|
| 684 |
try {
|
| 685 |
state.alerts = await api.getAlerts()
|
| 686 |
} catch (e) {
|
| 687 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 688 |
}
|
| 689 |
}
|
| 690 |
|
|
@@ -703,11 +598,15 @@ export async function init() {
|
|
| 703 |
})
|
| 704 |
})
|
| 705 |
|
|
|
|
|
|
|
|
|
|
| 706 |
await loadMarkets()
|
| 707 |
await loadSignals()
|
| 708 |
await loadPositions()
|
| 709 |
await loadWatchlist()
|
| 710 |
await loadAlerts()
|
|
|
|
| 711 |
|
| 712 |
map.init('map-container', state.markets, state.signals, selectMarket)
|
| 713 |
simulator.init(state)
|
|
@@ -723,7 +622,7 @@ export async function init() {
|
|
| 723 |
socket.on('market_update', (data) => {
|
| 724 |
const m = state.markets.find((x) => x.id === data.marketId)
|
| 725 |
if (m) {
|
| 726 |
-
//
|
| 727 |
if (typeof data.yesPrice === 'number') m.yesPrice = data.yesPrice
|
| 728 |
if (typeof data.noPrice === 'number') m.noPrice = data.noPrice
|
| 729 |
if (typeof data.volumeEur === 'number') m.volumeEur = data.volumeEur
|
|
@@ -748,16 +647,4 @@ export async function init() {
|
|
| 748 |
state.alerts.unshift(data)
|
| 749 |
if (state.view === 'alerts') renderAlerts()
|
| 750 |
})
|
| 751 |
-
|
| 752 |
-
setInterval(() => {
|
| 753 |
-
const statMarkets = document.getElementById('stat-markets')
|
| 754 |
-
if (statMarkets) {
|
| 755 |
-
const n = parseInt(statMarkets.textContent.replace(/\./g, '')) + Math.floor(Math.random() * 3)
|
| 756 |
-
statMarkets.textContent = n.toLocaleString('es-ES')
|
| 757 |
-
}
|
| 758 |
-
const statSignals = document.getElementById('stat-signals')
|
| 759 |
-
if (statSignals && Math.random() > 0.7) {
|
| 760 |
-
statSignals.textContent = parseInt(statSignals.textContent) + 1
|
| 761 |
-
}
|
| 762 |
-
}, 3000)
|
| 763 |
}
|
|
|
|
| 11 |
* * price_alert → anade alertas al historial.
|
| 12 |
* - Renderizado del panel de detalle de mercado con sparklines, grafico 7d,
|
| 13 |
* analisis IA y simulador de posiciones virtuales.
|
| 14 |
+
* - Auto-login con credenciales demo para endpoints protegidos.
|
| 15 |
*
|
| 16 |
* Modulos importados:
|
| 17 |
+
* - api.js → cliente REST del backend (con JWT).
|
| 18 |
* - charts.js → Chart.js (historial 7d + sparklines).
|
| 19 |
* - map.js → Leaflet (mapa mundial interactivo).
|
| 20 |
* - simulator.js → logica de compra/venta virtual.
|
|
|
|
| 44 |
sidebarCollapsed: false,
|
| 45 |
}
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
/* ─── Helpers ─── */
|
| 48 |
function formatCurrency(n) {
|
| 49 |
+
if (!n || n === 0) return '€0'
|
| 50 |
if (n >= 1e6) return '€' + (n / 1e6).toFixed(1) + 'M'
|
| 51 |
if (n >= 1e3) return '€' + (n / 1e3).toFixed(1) + 'K'
|
| 52 |
return '€' + n.toFixed(0)
|
| 53 |
}
|
| 54 |
|
| 55 |
function formatPrice(p) {
|
| 56 |
+
return Math.round((p || 0) * 100) + '¢'
|
| 57 |
}
|
| 58 |
|
| 59 |
function formatDate(iso) {
|
| 60 |
+
if (!iso) return '—'
|
| 61 |
const d = new Date(iso)
|
| 62 |
return d.toLocaleDateString('es-ES', { month: 'short', day: 'numeric', year: 'numeric' })
|
| 63 |
}
|
|
|
|
| 86 |
return 'neutral'
|
| 87 |
}
|
| 88 |
|
| 89 |
+
function translateOutcome(outcome) {
|
| 90 |
+
if (outcome === 'YES') return 'SÍ'
|
| 91 |
+
if (outcome === 'NO') return 'NO'
|
| 92 |
+
return outcome
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// Crea un elemento con una clase opcional y contenido de texto opcional
|
| 96 |
function el(tag, className, text) {
|
| 97 |
const node = document.createElement(tag)
|
| 98 |
if (className) node.className = className
|
|
|
|
| 104 |
return el('div', sm ? 'empty-state empty-state-sm' : 'empty-state', text)
|
| 105 |
}
|
| 106 |
|
| 107 |
+
/* ─── Auth ─── */
|
| 108 |
+
async function ensureAuth() {
|
| 109 |
+
if (api.isAuthenticated()) return true
|
| 110 |
+
try {
|
| 111 |
+
await api.login('user@polysignal.test', 'User123!')
|
| 112 |
+
return true
|
| 113 |
+
} catch (e) {
|
| 114 |
+
console.warn('Auto-login fallido:', e)
|
| 115 |
+
return false
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
/* ─── Routing de vistas ─── */
|
| 120 |
function switchView(viewName) {
|
| 121 |
state.view = viewName
|
|
|
|
| 147 |
if (!container) return
|
| 148 |
container.replaceChildren()
|
| 149 |
|
| 150 |
+
if (state.markets.length === 0) {
|
| 151 |
+
container.appendChild(emptyState('No hay mercados disponibles'))
|
| 152 |
+
return
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
state.markets.forEach((m) => {
|
| 156 |
+
const sig = state.signals.find((s) => s.marketId === m.id) || { signal: 'neutral', confidence: 0.5 }
|
| 157 |
const cls = signalColorClass(sig.signal)
|
| 158 |
|
| 159 |
const card = el('div', `market-card${state.activeMarketId === m.id ? ' active' : ''}`)
|
|
|
|
| 170 |
const probWrap = el('div', 'prob-bar-wrap')
|
| 171 |
const probBg = el('div', 'prob-bar-bg')
|
| 172 |
const probFill = el('div', `prob-bar-fill bg-${cls}`)
|
| 173 |
+
probFill.style.setProperty('--prob-width', `${Math.round((m.yesPrice || 0) * 100)}%`)
|
| 174 |
probBg.appendChild(probFill)
|
| 175 |
probWrap.appendChild(probBg)
|
| 176 |
|
|
|
|
| 199 |
|
| 200 |
let netPnl = 0
|
| 201 |
state.positions.forEach((p) => {
|
| 202 |
+
const m = state.markets.find((x) => x.id === p.marketId) || p.market || { question: p.marketId }
|
| 203 |
+
netPnl += p.pnl || 0
|
| 204 |
+
const cls = (p.pnl || 0) >= 0 ? 'green' : 'red'
|
| 205 |
+
const sign = (p.pnl || 0) >= 0 ? '+' : ''
|
| 206 |
|
| 207 |
const row = el('div', 'flex-between mb-6')
|
| 208 |
const label = el('span', 'text-sm text-neutral font-mono')
|
| 209 |
+
label.textContent = `${(m.question || p.marketId).substring(0, 32)}${(m.question || p.marketId).length > 32 ? '…' : ''} ${translateOutcome(p.outcome)}`
|
| 210 |
const val = el('span', `text-base font-semibold text-${cls} font-mono`)
|
| 211 |
+
val.textContent = `${sign}€${(p.pnl || 0).toFixed(2)}`
|
| 212 |
row.append(label, val)
|
| 213 |
container.appendChild(row)
|
| 214 |
})
|
|
|
|
| 233 |
return
|
| 234 |
}
|
| 235 |
|
| 236 |
+
const sig = state.signals.find((s) => s.marketId === m.id) || {
|
| 237 |
+
signal: 'neutral',
|
| 238 |
+
confidence: 0.5,
|
| 239 |
+
summary: 'Aún no hay análisis de IA disponible.',
|
| 240 |
+
keyRisk: '',
|
| 241 |
+
}
|
| 242 |
const delta = ((m.yesPrice - 0.5) * 20).toFixed(1)
|
| 243 |
+
const deltaCls = (m.yesPrice || 0) > 0.5 ? 'green' : 'red'
|
| 244 |
+
const deltaSign = (m.yesPrice || 0) > 0.5 ? '+' : ''
|
| 245 |
|
| 246 |
// ── Header ──
|
| 247 |
const tag = el('div', 'detail-tag')
|
|
|
|
| 275 |
const yesPriceEl = el('div', 'outcome-price')
|
| 276 |
yesPriceEl.textContent = formatPrice(m.yesPrice)
|
| 277 |
const yesDeltaEl = el('div', 'outcome-delta td-green')
|
| 278 |
+
yesDeltaEl.textContent = `▲ ${((m.yesPrice || 0) * 0.05).toFixed(1)}¢`
|
| 279 |
const yesCard = el('div', 'outcome-card yes')
|
| 280 |
yesCard.append(el('div', 'outcome-name', 'SÍ'), yesPriceEl, yesDeltaEl, sparkYes)
|
| 281 |
|
|
|
|
| 284 |
const noPriceEl = el('div', 'outcome-price')
|
| 285 |
noPriceEl.textContent = formatPrice(m.noPrice)
|
| 286 |
const noDeltaEl = el('div', 'outcome-delta td-red')
|
| 287 |
+
noDeltaEl.textContent = `▼ ${((m.noPrice || 0) * 0.05).toFixed(1)}¢`
|
| 288 |
const noCard = el('div', 'outcome-card no')
|
| 289 |
noCard.append(el('div', 'outcome-name', 'NO'), noPriceEl, noDeltaEl, sparkNo)
|
| 290 |
|
|
|
|
| 303 |
const aiHeader = el('div', 'flex-row gap-8 mb-4 flex-wrap')
|
| 304 |
aiHeader.append(el('div', 'ai-label', 'Análisis IA · HuggingFace Qwen3-8B'), aiBadge, aiMeta)
|
| 305 |
|
| 306 |
+
// Texto IA construido con nodos DOM — ninguna cadena externa toca innerHTML
|
| 307 |
const aiText = el('div', 'ai-text')
|
| 308 |
+
aiText.textContent = sig.summary || 'Aún no hay análisis de IA disponible.'
|
| 309 |
if (sig.keyRisk) {
|
| 310 |
const strong = document.createElement('strong')
|
| 311 |
strong.textContent = 'Riesgo clave:'
|
|
|
|
| 341 |
|
| 342 |
container.replaceChildren(header, outcomesRow, aiBox, simRow)
|
| 343 |
|
| 344 |
+
// Vincula botones del simulador
|
| 345 |
+
simYes.addEventListener('click', () => simulator.openPosition(m.id, 'YES', simAmount.value))
|
| 346 |
simNo.addEventListener('click', () => simulator.openPosition(m.id, 'NO', simAmount.value))
|
| 347 |
|
| 348 |
+
// Renderiza graficos
|
| 349 |
charts.renderDetailChart('detail-chart', m.yesPrice)
|
| 350 |
charts.renderSparkline('spark-yes', m.yesPrice, 'yes')
|
| 351 |
charts.renderSparkline('spark-no', m.noPrice, 'no')
|
|
|
|
| 373 |
tbody.replaceChildren()
|
| 374 |
|
| 375 |
state.positions.forEach((p) => {
|
| 376 |
+
const m = state.markets.find((x) => x.id === p.marketId) || p.market || { question: p.marketId }
|
| 377 |
+
const pnlColor = (p.pnl || 0) >= 0 ? 'td-green' : 'td-red'
|
| 378 |
+
const sign = (p.pnl || 0) >= 0 ? '+' : ''
|
| 379 |
|
| 380 |
const tr = document.createElement('tr')
|
| 381 |
|
| 382 |
const tdQ = el('td')
|
| 383 |
+
tdQ.textContent = `${(m.question || p.marketId).substring(0, 40)}${(m.question || p.marketId).length > 40 ? '…' : ''}`
|
| 384 |
|
| 385 |
+
const tdOutcome = el('td', `td-mono ${p.outcome === 'YES' ? 'td-green' : 'td-red'}`)
|
| 386 |
+
tdOutcome.textContent = translateOutcome(p.outcome)
|
| 387 |
|
| 388 |
const tdAmt = el('td', 'td-mono')
|
| 389 |
tdAmt.textContent = `€${p.amountEur.toFixed(0)}`
|
|
|
|
| 395 |
tdCurrent.textContent = formatPrice(p.currentPrice)
|
| 396 |
|
| 397 |
const tdPnl = el('td', `td-mono ${pnlColor}`)
|
| 398 |
+
tdPnl.textContent = `${sign}€${(p.pnl || 0).toFixed(2)}`
|
| 399 |
|
| 400 |
const tdKelly = el('td', 'td-mono td-blue')
|
| 401 |
tdKelly.textContent = `${((p.kellyFraction || 0) * 100).toFixed(0)}%`
|
|
|
|
| 434 |
tbody.replaceChildren()
|
| 435 |
|
| 436 |
state.watchlist.forEach((w) => {
|
| 437 |
+
const m = state.markets.find((x) => x.id === w.marketId) || w.market || { question: w.marketId, category: '-', yesPrice: 0, noPrice: 0, volumeEur: 0 }
|
| 438 |
const sig = state.signals.find((s) => s.marketId === w.marketId) || { signal: 'neutral' }
|
| 439 |
|
| 440 |
const tr = document.createElement('tr')
|
| 441 |
|
| 442 |
const tdQ = el('td')
|
| 443 |
+
tdQ.textContent = `${(m.question || w.marketId).substring(0, 40)}${(m.question || w.marketId).length > 40 ? '…' : ''}`
|
| 444 |
|
| 445 |
const tdCat = el('td')
|
| 446 |
tdCat.textContent = m.category || '-'
|
|
|
|
| 492 |
tbody.replaceChildren()
|
| 493 |
|
| 494 |
state.alerts.forEach((a) => {
|
| 495 |
+
const m = state.markets.find((x) => x.id === a.marketId) || a.market || { question: a.marketId }
|
| 496 |
|
| 497 |
const tr = document.createElement('tr')
|
| 498 |
|
|
|
|
| 500 |
tdDate.textContent = new Date(a.sentAt).toLocaleString('es-ES')
|
| 501 |
|
| 502 |
const tdQ = el('td')
|
| 503 |
+
tdQ.textContent = `${(m.question || a.marketId).substring(0, 35)}${(m.question || a.marketId).length > 35 ? '…' : ''}`
|
| 504 |
|
| 505 |
const typeBadge = el('span', 'signal-badge sig-neut')
|
| 506 |
typeBadge.textContent = a.type
|
|
|
|
| 518 |
/* ─── Carga de datos ─── */
|
| 519 |
async function loadMarkets() {
|
| 520 |
try {
|
| 521 |
+
state.markets = await api.getMarkets({ limit: 50 })
|
|
|
|
| 522 |
} catch (e) {
|
| 523 |
+
console.error('Error cargando mercados:', e)
|
| 524 |
+
state.markets = []
|
| 525 |
}
|
| 526 |
}
|
| 527 |
|
| 528 |
async function loadSignals() {
|
| 529 |
+
if (state.markets.length === 0) {
|
| 530 |
+
state.signals = []
|
| 531 |
+
return
|
| 532 |
+
}
|
| 533 |
try {
|
| 534 |
+
const marketIds = state.markets.map((m) => m.id)
|
| 535 |
+
const results = await api.getSignalsBatch(marketIds)
|
| 536 |
+
state.signals = results.map((r) => ({ ...r, marketId: r.marketId }))
|
| 537 |
} catch (e) {
|
| 538 |
+
console.error('Error cargando señales:', e)
|
| 539 |
+
state.signals = []
|
| 540 |
}
|
| 541 |
}
|
| 542 |
|
|
|
|
| 544 |
try {
|
| 545 |
state.positions = await api.getPositions()
|
| 546 |
} catch (e) {
|
| 547 |
+
console.error('Error cargando posiciones:', e)
|
| 548 |
+
state.positions = []
|
| 549 |
}
|
| 550 |
}
|
| 551 |
|
|
|
|
| 553 |
try {
|
| 554 |
state.watchlist = await api.getWatchlist()
|
| 555 |
} catch (e) {
|
| 556 |
+
console.error('Error cargando watchlist:', e)
|
| 557 |
state.watchlist = []
|
| 558 |
}
|
| 559 |
}
|
|
|
|
| 562 |
try {
|
| 563 |
state.alerts = await api.getAlerts()
|
| 564 |
} catch (e) {
|
| 565 |
+
console.error('Error cargando alertas:', e)
|
| 566 |
+
state.alerts = []
|
| 567 |
+
}
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
async function loadStats() {
|
| 571 |
+
try {
|
| 572 |
+
const stats = await api.getStats()
|
| 573 |
+
const statMarkets = document.getElementById('stat-markets')
|
| 574 |
+
if (statMarkets) statMarkets.textContent = (stats.marketsCount ?? 0).toLocaleString('es-ES')
|
| 575 |
+
const statVolume = document.getElementById('stat-volume')
|
| 576 |
+
if (statVolume) statVolume.textContent = formatCurrency(stats.volume24h || 0)
|
| 577 |
+
const statSignals = document.getElementById('stat-signals')
|
| 578 |
+
if (statSignals) statSignals.textContent = stats.signalsCount ?? 0
|
| 579 |
+
const statAlerts = document.getElementById('stat-alerts')
|
| 580 |
+
if (statAlerts) statAlerts.textContent = stats.alertsToday ?? 0
|
| 581 |
+
} catch (e) {
|
| 582 |
+
console.error('Error cargando stats:', e)
|
| 583 |
}
|
| 584 |
}
|
| 585 |
|
|
|
|
| 598 |
})
|
| 599 |
})
|
| 600 |
|
| 601 |
+
// Auto-login con credenciales demo antes de cargar datos protegidos
|
| 602 |
+
await ensureAuth()
|
| 603 |
+
|
| 604 |
await loadMarkets()
|
| 605 |
await loadSignals()
|
| 606 |
await loadPositions()
|
| 607 |
await loadWatchlist()
|
| 608 |
await loadAlerts()
|
| 609 |
+
await loadStats()
|
| 610 |
|
| 611 |
map.init('map-container', state.markets, state.signals, selectMarket)
|
| 612 |
simulator.init(state)
|
|
|
|
| 622 |
socket.on('market_update', (data) => {
|
| 623 |
const m = state.markets.find((x) => x.id === data.marketId)
|
| 624 |
if (m) {
|
| 625 |
+
// Solo copia campos numericos conocidos — nunca mezcle todo el payload
|
| 626 |
if (typeof data.yesPrice === 'number') m.yesPrice = data.yesPrice
|
| 627 |
if (typeof data.noPrice === 'number') m.noPrice = data.noPrice
|
| 628 |
if (typeof data.volumeEur === 'number') m.volumeEur = data.volumeEur
|
|
|
|
| 647 |
state.alerts.unshift(data)
|
| 648 |
if (state.view === 'alerts') renderAlerts()
|
| 649 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 650 |
}
|
frontend/src/capitals.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Utilidades de geolocalización basadas en la aportación de Arnau.
|
| 3 |
+
* Importa datos de capitales desde JSON estático y construye índices
|
| 4 |
+
* para búsqueda O(1) por código ISO y búsqueda por nombre.
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import capitals from './capitals.json'
|
| 8 |
+
|
| 9 |
+
// Índice para búsqueda O(1) por código ISO
|
| 10 |
+
const coordsByCode = new Map()
|
| 11 |
+
for (const c of capitals) {
|
| 12 |
+
if (c.pais_codigo) coordsByCode.set(c.pais_codigo.toUpperCase(), [c.latitud, c.longitud])
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
// Índice para búsqueda de respaldo por nombre (país y capital)
|
| 16 |
+
const nameIndex = new Map()
|
| 17 |
+
for (const c of capitals) {
|
| 18 |
+
nameIndex.set(c.pais.toLowerCase(), [c.latitud, c.longitud])
|
| 19 |
+
nameIndex.set(c.capital.toLowerCase(), [c.latitud, c.longitud])
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
/**
|
| 23 |
+
* Obtiene coordenadas por código de país ISO2.
|
| 24 |
+
* Recurre al dataset expandido de Arnau si no está en la lista hardcoded.
|
| 25 |
+
*/
|
| 26 |
+
export function getCoordsByCode(countryCode) {
|
| 27 |
+
const code = countryCode?.toUpperCase()
|
| 28 |
+
if (!code) return null
|
| 29 |
+
|
| 30 |
+
// Resguardo heredado hardcoded (mantenido por compatibilidad)
|
| 31 |
+
const legacy = {
|
| 32 |
+
US: [37.09, -95.71], DE: [51.16, 10.45], GB: [55.37, -3.43],
|
| 33 |
+
BR: [-14.23, -51.92], CN: [35.86, 104.19], IN: [20.59, 78.96],
|
| 34 |
+
KR: [35.90, 127.76], SA: [23.88, 45.07], FR: [46.22, 2.21],
|
| 35 |
+
JP: [36.20, 138.25], AU: [-25.27, 133.77], CA: [56.13, -106.34],
|
| 36 |
+
RU: [61.52, 105.31], MX: [23.63, -102.55], ZA: [-30.55, 22.93],
|
| 37 |
+
}
|
| 38 |
+
return legacy[code] || coordsByCode.get(code) || null
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
/**
|
| 42 |
+
* Detecta país en el texto de la pregunta de un mercado.
|
| 43 |
+
* Devuelve coordenadas si se encuentra un país o capital conocido, null en caso contrario.
|
| 44 |
+
* Seguro: solo lee texto, sin inyección DOM.
|
| 45 |
+
*/
|
| 46 |
+
export function detectCountryInText(text) {
|
| 47 |
+
if (!text) return null
|
| 48 |
+
const lower = text.toLowerCase()
|
| 49 |
+
for (const [key, coords] of nameIndex) {
|
| 50 |
+
if (lower.includes(key)) return coords
|
| 51 |
+
}
|
| 52 |
+
return null
|
| 53 |
+
}
|
frontend/src/capitals.json
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{ "pais_codigo": "GB", "pais": "United Kingdom", "capital": "London", "latitud": 51.5074456, "longitud": -0.1277653 },
|
| 3 |
+
{ "pais_codigo": "NZ", "pais": "New Zealand", "capital": "Wellington", "latitud": -41.2887953, "longitud": 174.7772114 },
|
| 4 |
+
{ "pais_codigo": "GR", "pais": "Greece", "capital": "Athens", "latitud": 37.9755648, "longitud": 23.7348324 },
|
| 5 |
+
{ "pais_codigo": "SI", "pais": "Slovenia", "capital": "Ljubljana", "latitud": 46.0500268, "longitud": 14.5069289 },
|
| 6 |
+
{ "pais_codigo": "DK", "pais": "Denmark", "capital": "Kobenhavn", "latitud": 55.6867243, "longitud": 12.5700724 },
|
| 7 |
+
{ "pais_codigo": "IN", "pais": "India", "capital": "New Delhi", "latitud": 28.6138954, "longitud": 77.2090057 },
|
| 8 |
+
{ "pais_codigo": "AT", "pais": "Austria", "capital": "Vienna", "latitud": 48.2083537, "longitud": 16.3725042 },
|
| 9 |
+
{ "pais_codigo": "FR", "pais": "France", "capital": "Paris", "latitud": 48.8534951, "longitud": 2.3483915 },
|
| 10 |
+
{ "pais_codigo": "CH", "pais": "Switzerland", "capital": "Bern", "latitud": 46.9484742, "longitud": 7.4521749 },
|
| 11 |
+
{ "pais_codigo": "CA", "pais": "Canada", "capital": "Ottawa", "latitud": 45.4208777, "longitud": -75.6901106 },
|
| 12 |
+
{ "pais_codigo": "IQ", "pais": "Irak", "capital": "Baghdad", "latitud": 33.3061701, "longitud": 44.3872213 },
|
| 13 |
+
{ "pais_codigo": "ES", "pais": "Spain", "capital": "Madrid", "latitud": 40.416782, "longitud": -3.703507 },
|
| 14 |
+
{ "pais_codigo": "AU", "pais": "Australia", "capital": "Canberra", "latitud": -35.2975906, "longitud": 149.1012676 },
|
| 15 |
+
{ "pais_codigo": "AF", "pais": "Afganistan", "capital": "Kabul", "latitud": 34.5269503, "longitud": 69.1850584 },
|
| 16 |
+
{ "pais_codigo": "CN", "pais": "China", "capital": "Pekin", "latitud": 39.9057136, "longitud": 116.3912972 },
|
| 17 |
+
{ "pais_codigo": "ZA", "pais": "South Africa", "capital": "Praetorship", "latitud": -25.7459277, "longitud": 28.1879101 },
|
| 18 |
+
{ "pais_codigo": "TR", "pais": "Turkey", "capital": "Ankara", "latitud": 39.9207759, "longitud": 32.8540497 },
|
| 19 |
+
{ "pais_codigo": "SE", "pais": "Sweden", "capital": "Stockholm", "latitud": 59.3251172, "longitud": 18.0710935 },
|
| 20 |
+
{ "pais_codigo": "IR", "pais": "Iran", "capital": "Tehran", "latitud": 35.6892523, "longitud": 51.3896004 },
|
| 21 |
+
{ "pais_codigo": "UA", "pais": "Ukraine", "capital": "Київ", "latitud": 50.4500336, "longitud": 30.5241361 },
|
| 22 |
+
{ "pais_codigo": "BY", "pais": "Belarus", "capital": "Мінск", "latitud": 53.9024716, "longitud": 27.5618225 },
|
| 23 |
+
{ "pais_codigo": "CU", "pais": "Cuba", "capital": "Havana", "latitud": 23.135305, "longitud": -82.3589631 },
|
| 24 |
+
{ "pais_codigo": "LV", "pais": "Letonia", "capital": "Rīga", "latitud": 56.9493977, "longitud": 24.1051846 },
|
| 25 |
+
{ "pais_codigo": "CD", "pais": "Democratic Republic of the Congo", "capital": "Kinshasa", "latitud": -4.32171, "longitud": 15.3122511 },
|
| 26 |
+
{ "pais_codigo": "LT", "pais": "Lituania", "capital": "Vilnius", "latitud": 54.6870458, "longitud": 25.2829111 },
|
| 27 |
+
{ "pais_codigo": "KG", "pais": "Kirguistan", "capital": "Бишкек", "latitud": 42.8761424, "longitud": 74.6036724 },
|
| 28 |
+
{ "pais_codigo": "UZ", "pais": "Uzbekistan", "capital": "Toshkent", "latitud": 41.3123363, "longitud": 69.2787079 },
|
| 29 |
+
{ "pais_codigo": "TJ", "pais": "Tayikistan", "capital": "Душанбе", "latitud": 38.5767045, "longitud": 68.785433 },
|
| 30 |
+
{ "pais_codigo": "MN", "pais": "Mongolia", "capital": "Ulaanbaatar", "latitud": 47.9184676, "longitud": 106.9177016 },
|
| 31 |
+
{ "pais_codigo": "KZ", "pais": "Kazajistan", "capital": "Астана", "latitud": 51.1282205, "longitud": 71.4306682 },
|
| 32 |
+
{ "pais_codigo": "NE", "pais": "Niger", "capital": "Niamey", "latitud": 13.524834, "longitud": 2.109823 },
|
| 33 |
+
{ "pais_codigo": "AO", "pais": "Angola", "capital": "Luanda", "latitud": -8.8272699, "longitud": 13.2439512 },
|
| 34 |
+
{ "pais_codigo": "ML", "pais": "Mali", "capital": "Bamako", "latitud": 12.649319, "longitud": -8.000337 },
|
| 35 |
+
{ "pais_codigo": "TN", "pais": "Tunisia", "capital": "Tunisia", "latitud": 36.8002068, "longitud": 10.1857757 },
|
| 36 |
+
{ "pais_codigo": "LR", "pais": "Liberia", "capital": "Monrovia", "latitud": 6.3203562, "longitud": -10.8060492 },
|
| 37 |
+
{ "pais_codigo": "CF", "pais": "Republica Centroafricana", "capital": "Bangui", "latitud": 4.3635118, "longitud": 18.5835913 },
|
| 38 |
+
{ "pais_codigo": "ZM", "pais": "Zambia", "capital": "Lusaka", "latitud": -15.4163395, "longitud": 28.2818414 },
|
| 39 |
+
{ "pais_codigo": "BF", "pais": "Burkina Faso", "capital": "Ouagadougou", "latitud": 12.3681873, "longitud": -1.5270944 },
|
| 40 |
+
{ "pais_codigo": "TD", "pais": "Chad", "capital": "N'Djaména", "latitud": 12.1191543, "longitud": 15.0502758 },
|
| 41 |
+
{ "pais_codigo": "SN", "pais": "Senegal", "capital": "Dakar", "latitud": 14.693425, "longitud": -17.447938 },
|
| 42 |
+
{ "pais_codigo": "SL", "pais": "Sierra Leona", "capital": "Freetown", "latitud": 8.479004, "longitud": -13.26795 },
|
| 43 |
+
{ "pais_codigo": "GN", "pais": "Guinea", "capital": "Conakry", "latitud": 9.5170602, "longitud": -13.6998434 },
|
| 44 |
+
{ "pais_codigo": "GA", "pais": "Gabon", "capital": "Libreville", "latitud": 0.4086518, "longitud": 9.4418849 },
|
| 45 |
+
{ "pais_codigo": "ET", "pais": "Ethiopia", "capital": "Addis Ababa", "latitud": 9.0358287, "longitud": 38.7524127 },
|
| 46 |
+
{ "pais_codigo": "GH", "pais": "Ghana", "capital": "Accra", "latitud": 5.5571096, "longitud": -0.2012376 },
|
| 47 |
+
{ "pais_codigo": "MZ", "pais": "Mozambique", "capital": "Maputo", "latitud": -25.966213, "longitud": 32.56745 },
|
| 48 |
+
{ "pais_codigo": "DJ", "pais": "Yibuti", "capital": "Djibouti", "latitud": 11.5936903, "longitud": 43.1472724 },
|
| 49 |
+
{ "pais_codigo": "KE", "pais": "Kenia", "capital": "Nairobi", "latitud": -1.2890006, "longitud": 36.8172812 },
|
| 50 |
+
{ "pais_codigo": "MG", "pais": "Madagascar", "capital": "Antananarivo", "latitud": -18.9100122, "longitud": 47.5255809 },
|
| 51 |
+
{ "pais_codigo": "SD", "pais": "Sudan", "capital": "Khartoum", "latitud": 15.5635972, "longitud": 32.5349123 },
|
| 52 |
+
{ "pais_codigo": "CV", "pais": "Cape Verde", "capital": "Praia", "latitud": 14.9162811, "longitud": -23.5095095 },
|
| 53 |
+
{ "pais_codigo": "IL", "pais": "Israel", "capital": "Jerusalen", "latitud": 31.7788472, "longitud": 35.2257856 },
|
| 54 |
+
{ "pais_codigo": "EE", "pais": "Estonia", "capital": "Tallinn", "latitud": 59.437242, "longitud": 24.7572693 },
|
| 55 |
+
{ "pais_codigo": "ID", "pais": "Indonesia", "capital": "Jakarta", "latitud": -6.1754049, "longitud": 106.827168 },
|
| 56 |
+
{ "pais_codigo": "LS", "pais": "Lesoto", "capital": "Maseru", "latitud": -29.310054, "longitud": 27.478222 },
|
| 57 |
+
{ "pais_codigo": "BW", "pais": "Botswana", "capital": "Gaborone", "latitud": -24.6581357, "longitud": 25.9088474 },
|
| 58 |
+
{ "pais_codigo": "NG", "pais": "Nigeria", "capital": "Abuja", "latitud": 9.0643305, "longitud": 7.4892974 },
|
| 59 |
+
{ "pais_codigo": "KP", "pais": "North Korea", "capital": "Pyongyang", "latitud": 39.0167979, "longitud": 125.7473609 },
|
| 60 |
+
{ "pais_codigo": "NA", "pais": "Namibia", "capital": "Windhoek", "latitud": -22.5776104, "longitud": 17.0772739 },
|
| 61 |
+
{ "pais_codigo": "SO", "pais": "Somalia", "capital": "Mogadishu", "latitud": 2.0349312, "longitud": 45.3419183 },
|
| 62 |
+
{ "pais_codigo": "BH", "pais": "Bahrain", "capital": "Manama", "latitud": 26.2235041, "longitud": 50.5822436 },
|
| 63 |
+
{ "pais_codigo": "CL", "pais": "Chile", "capital": "Santiago", "latitud": -33.4376995, "longitud": -70.6510671 },
|
| 64 |
+
{ "pais_codigo": "LU", "pais": "Luxembourg", "capital": "Luxembourg", "latitud": 49.6112768, "longitud": 6.129799 },
|
| 65 |
+
{ "pais_codigo": "JM", "pais": "Jamaica", "capital": "Kingston", "latitud": 17.9712148, "longitud": -76.7928128 },
|
| 66 |
+
{ "pais_codigo": "EC", "pais": "Ecuador", "capital": "Quito", "latitud": -0.2201641, "longitud": -78.5123274 },
|
| 67 |
+
{ "pais_codigo": "AD", "pais": "Andorra", "capital": "Andorra la Vella", "latitud": 42.5069391, "longitud": 1.5212467 },
|
| 68 |
+
{ "pais_codigo": "RW", "pais": "Ruanda", "capital": "Kigali", "latitud": -1.9534357, "longitud": 30.1140089 },
|
| 69 |
+
{ "pais_codigo": "RS", "pais": "Serbia", "capital": "Belgrade", "latitud": 44.8178131, "longitud": 20.4568974 },
|
| 70 |
+
{ "pais_codigo": "MX", "pais": "Mexico", "capital": "Mexico City", "latitud": 19.4326296, "longitud": -99.1331785 },
|
| 71 |
+
{ "pais_codigo": "NP", "pais": "Nepal", "capital": "Kathmandu", "latitud": 27.708317, "longitud": 85.3205817 },
|
| 72 |
+
{ "pais_codigo": "IT", "pais": "Italy", "capital": "Rome", "latitud": 41.8933203, "longitud": 12.4829321 },
|
| 73 |
+
{ "pais_codigo": "VN", "pais": "Vietnam", "capital": "Hanoi", "latitud": 21.0283334, "longitud": 105.854041 },
|
| 74 |
+
{ "pais_codigo": "AR", "pais": "Argentina", "capital": "Buenos Aires", "latitud": -34.6095579, "longitud": -58.3887904 },
|
| 75 |
+
{ "pais_codigo": "HU", "pais": "Hungary", "capital": "Budapest", "latitud": 47.4978789, "longitud": 19.0402383 },
|
| 76 |
+
{ "pais_codigo": "TZ", "pais": "Tanzania", "capital": "Dodoma", "latitud": -6.1791181, "longitud": 35.7468174 },
|
| 77 |
+
{ "pais_codigo": "RO", "pais": "Rumania", "capital": "Bucharest", "latitud": 44.4361414, "longitud": 26.102684 },
|
| 78 |
+
{ "pais_codigo": "SS", "pais": "South Sudan", "capital": "Juba", "latitud": 4.8459246, "longitud": 31.5959173 },
|
| 79 |
+
{ "pais_codigo": "VU", "pais": "Vanuatu", "capital": "Port Vila", "latitud": -17.7414972, "longitud": 168.3150163 },
|
| 80 |
+
{ "pais_codigo": "BS", "pais": "Bahamas", "capital": "Nassau", "latitud": 25.0782266, "longitud": -77.3383438 },
|
| 81 |
+
{ "pais_codigo": "LA", "pais": "Laos", "capital": "Vientian", "latitud": 17.9640988, "longitud": 102.6133707 },
|
| 82 |
+
{ "pais_codigo": "NO", "pais": "Norway", "capital": "Oslo", "latitud": 59.8937521, "longitud": 10.6203118 },
|
| 83 |
+
{ "pais_codigo": "ME", "pais": "Montenegro", "capital": "Podgorica", "latitud": 42.4415238, "longitud": 19.2621081 },
|
| 84 |
+
{ "pais_codigo": "MD", "pais": "Moldavia", "capital": "Chişinău", "latitud": 47.0245117, "longitud": 28.8322923 },
|
| 85 |
+
{ "pais_codigo": "AM", "pais": "Armenia", "capital": "Երևան", "latitud": 40.1777112, "longitud": 44.5126233 },
|
| 86 |
+
{ "pais_codigo": "US", "pais": "United States", "capital": "Washington", "latitud": 38.8950982, "longitud": -77.0363849 },
|
| 87 |
+
{ "pais_codigo": "MK", "pais": "North Macedonia", "capital": "Скопје", "latitud": 41.9962164, "longitud": 21.4318935 },
|
| 88 |
+
{ "pais_codigo": "PH", "pais": "Philippines", "capital": "Manila", "latitud": 14.5904492, "longitud": 120.9803621 },
|
| 89 |
+
{ "pais_codigo": "OM", "pais": "Oman", "capital": "Mascate", "latitud": 23.6123628, "longitud": 58.5938134 },
|
| 90 |
+
{ "pais_codigo": "PG", "pais": "Papua New Guinea", "capital": "Port Moresby", "latitud": -9.4743301, "longitud": 147.1599504 },
|
| 91 |
+
{ "pais_codigo": "DE", "pais": "Germany", "capital": "Berlin", "latitud": 52.5173885, "longitud": 13.3951309 },
|
| 92 |
+
{ "pais_codigo": "MU", "pais": "Mauricio", "capital": "Port Louis", "latitud": -20.1624522, "longitud": 57.5028044 },
|
| 93 |
+
{ "pais_codigo": "SZ", "pais": "Swaziland", "capital": "Mbabane", "latitud": -26.325745, "longitud": 31.144663 },
|
| 94 |
+
{ "pais_codigo": "GD", "pais": "Grenada", "capital": "St. George's", "latitud": 12.0535331, "longitud": -61.751805 },
|
| 95 |
+
{ "pais_codigo": "VC", "pais": "Saint Vincent and the Grenadines", "capital": "Kingstown", "latitud": 13.1561864, "longitud": -61.2279621 },
|
| 96 |
+
{ "pais_codigo": "BB", "pais": "Barbados", "capital": "Bridgetown", "latitud": 13.0977832, "longitud": -59.6184184 },
|
| 97 |
+
{ "pais_codigo": "MR", "pais": "Mauritius", "capital": "Nuakchot", "latitud": 18.0792379, "longitud": -15.9780071 },
|
| 98 |
+
{ "pais_codigo": "GM", "pais": "Gambia", "capital": "Banjul", "latitud": 13.45535, "longitud": -16.575646 },
|
| 99 |
+
{ "pais_codigo": "PY", "pais": "Paraguay", "capital": "Asuncion", "latitud": -25.2800459, "longitud": -57.6343814 },
|
| 100 |
+
{ "pais_codigo": "GW", "pais": "Guinea Bissau", "capital": "Bissau", "latitud": 11.861324, "longitud": -15.583055 },
|
| 101 |
+
{ "pais_codigo": "YE", "pais": "Yemen", "capital": "Fury", "latitud": 15.3538569, "longitud": 44.2058841 },
|
| 102 |
+
{ "pais_codigo": "AE", "pais": "United Arab Emirates", "capital": "Abu Dhabi", "latitud": 24.4538352, "longitud": 54.3774014 },
|
| 103 |
+
{ "pais_codigo": "TL", "pais": "Timor-Leste", "capital": "Dili", "latitud": -8.5536809, "longitud": 125.5784093 },
|
| 104 |
+
{ "pais_codigo": "JP", "pais": "Japan", "capital": "Tokio", "latitud": 35.6768601, "longitud": 139.7638947 },
|
| 105 |
+
{ "pais_codigo": "PT", "pais": "Portugal", "capital": "Lisbon", "latitud": 38.7077507, "longitud": -9.1365919 },
|
| 106 |
+
{ "pais_codigo": "CK", "pais": "Islas Cook", "capital": "Avarua", "latitud": -21.2074736, "longitud": -159.7708145 },
|
| 107 |
+
{ "pais_codigo": "NL", "pais": "Netherlands", "capital": "Amsterdam", "latitud": 52.3730796, "longitud": 4.8924534 },
|
| 108 |
+
{ "pais_codigo": "EG", "pais": "Egypt", "capital": "Cairo", "latitud": 30.0443879, "longitud": 31.2357257 },
|
| 109 |
+
{ "pais_codigo": "SM", "pais": "Saint Marino", "capital": "City of Saint Marino", "latitud": 43.9363996, "longitud": 12.4466991 },
|
| 110 |
+
{ "pais_codigo": "SR", "pais": "Suriname", "capital": "Paramaribo", "latitud": 5.8247628, "longitud": -55.1703941 },
|
| 111 |
+
{ "pais_codigo": "BZ", "pais": "Belize", "capital": "Belmopan", "latitud": 17.250199, "longitud": -88.770018 },
|
| 112 |
+
{ "pais_codigo": "ZW", "pais": "Zimbawe", "capital": "Harare", "latitud": -17.831773, "longitud": 31.045686 },
|
| 113 |
+
{ "pais_codigo": "HT", "pais": "Haiti", "capital": "Port-au-Prince", "latitud": 18.547327, "longitud": -72.3395928 },
|
| 114 |
+
{ "pais_codigo": "LB", "pais": "Lebanon", "capital": "Beirut", "latitud": 33.8959203, "longitud": 35.47843 },
|
| 115 |
+
{ "pais_codigo": "GY", "pais": "Guyana", "capital": "Georgetown", "latitud": 6.8032561, "longitud": -58.1455403 },
|
| 116 |
+
{ "pais_codigo": "ST", "pais": "São Tomé and Principe", "capital": "São Tomé", "latitud": 0.3389242, "longitud": 6.7313031 },
|
| 117 |
+
{ "pais_codigo": "UY", "pais": "Uruguay", "capital": "Montevideo", "latitud": -34.9058916, "longitud": -56.1913095 },
|
| 118 |
+
{ "pais_codigo": "MA", "pais": "Morocco", "capital": "Rabat", "latitud": 34.0218454, "longitud": -6.8408929 },
|
| 119 |
+
{ "pais_codigo": "DZ", "pais": "Algeria", "capital": "Algiers", "latitud": 36.7729333, "longitud": 3.0588445 },
|
| 120 |
+
{ "pais_codigo": "BI", "pais": "Burundi", "capital": "Gitega", "latitud": -3.4284953, "longitud": 29.9249718 },
|
| 121 |
+
{ "pais_codigo": "BO", "pais": "Bolivia", "capital": "Sucre", "latitud": -19.0477251, "longitud": -65.2594306 },
|
| 122 |
+
{ "pais_codigo": "DO", "pais": "Dominican Republic", "capital": "Santo Domingo", "latitud": 18.4713858, "longitud": -69.8918436 },
|
| 123 |
+
{ "pais_codigo": "SA", "pais": "Saudi Arabia", "capital": "Riyadh", "latitud": 24.638916, "longitud": 46.7160104 },
|
| 124 |
+
{ "pais_codigo": "MM", "pais": "Myanmar", "capital": "Naypyidaw", "latitud": 19.7753289, "longitud": 96.1032552 },
|
| 125 |
+
{ "pais_codigo": "PK", "pais": "Pakistan", "capital": "Islamabad", "latitud": 33.6938118, "longitud": 73.0651511 },
|
| 126 |
+
{ "pais_codigo": "CI", "pais": "Ivory Coast", "capital": "Yamoussoukro", "latitud": 6.8200066, "longitud": -5.2776034 },
|
| 127 |
+
{ "pais_codigo": "CM", "pais": "Cameroon", "capital": "Yaoundé", "latitud": 3.8689867, "longitud": 11.5213344 },
|
| 128 |
+
{ "pais_codigo": "KM", "pais": "Comoras", "capital": "Moroni", "latitud": -11.7040306, "longitud": 43.251909 },
|
| 129 |
+
{ "pais_codigo": "MH", "pais": "Marshall Islands", "capital": "Majuro", "latitud": 7.0909924, "longitud": 171.3816354 },
|
| 130 |
+
{ "pais_codigo": "WS", "pais": "Samoa", "capital": "Apia", "latitud": -13.8345235, "longitud": -171.7630955 },
|
| 131 |
+
{ "pais_codigo": "PL", "pais": "Poland", "capital": "Warsaw", "latitud": 52.2319581, "longitud": 21.0067249 },
|
| 132 |
+
{ "pais_codigo": "CG", "pais": "Congo", "capital": "Brazzaville", "latitud": -4.2694407, "longitud": 15.2712256 },
|
| 133 |
+
{ "pais_codigo": "NU", "pais": "Niue", "capital": "Alofi", "latitud": -19.0534159, "longitud": -169.919199 },
|
| 134 |
+
{ "pais_codigo": "GT", "pais": "Guatemala", "capital": "Guatemala City", "latitud": 14.6416142, "longitud": -90.5132836 },
|
| 135 |
+
{ "pais_codigo": "SK", "pais": "Slovakia", "capital": "Bratislava", "latitud": 48.1516988, "longitud": 17.1093063 },
|
| 136 |
+
{ "pais_codigo": "SG", "pais": "Singapore", "capital": "Singapore", "latitud": 1.2899175, "longitud": 103.8519072 },
|
| 137 |
+
{ "pais_codigo": "KW", "pais": "Kuwait", "capital": "Kuwait", "latitud": 29.3796532, "longitud": 47.9734174 },
|
| 138 |
+
{ "pais_codigo": "KH", "pais": "Cambodia", "capital": "Nom Pen", "latitud": 11.568271, "longitud": 104.9224426 },
|
| 139 |
+
{ "pais_codigo": "CO", "pais": "Colombia", "capital": "Bogotá", "latitud": 4.6533817, "longitud": -74.0836331 },
|
| 140 |
+
{ "pais_codigo": "KI", "pais": "Kiribati", "capital": "South Tarawa", "latitud": 1.3490778, "longitud": 173.0386512 },
|
| 141 |
+
{ "pais_codigo": "FM", "pais": "Micronesia", "capital": "Palikir", "latitud": 6.920744, "longitud": 158.1627143 },
|
| 142 |
+
{ "pais_codigo": "UG", "pais": "Uganda", "capital": "Kampala", "latitud": 0.3177137, "longitud": 32.5813539 },
|
| 143 |
+
{ "pais_codigo": "SB", "pais": "Solomon Islands", "capital": "Honiara", "latitud": -9.4310769, "longitud": 159.9552552 },
|
| 144 |
+
{ "pais_codigo": "VE", "pais": "Venezuela", "capital": "Caracas", "latitud": 10.5060934, "longitud": -66.9146008 },
|
| 145 |
+
{ "pais_codigo": "GL", "pais": "Greenland", "capital": "Nuuk", "latitud": 64.1767049, "longitud": -51.7361444 },
|
| 146 |
+
{ "pais_codigo": "TO", "pais": "Tonga", "capital": "Nukuʻalofa", "latitud": -21.1343401, "longitud": -175.201808 },
|
| 147 |
+
{ "pais_codigo": "SY", "pais": "Syria", "capital": "Damascus", "latitud": 33.5130695, "longitud": 36.3095814 },
|
| 148 |
+
{ "pais_codigo": "TW", "pais": "Taiwan", "capital": "Taipei", "latitud": 25.0375198, "longitud": 121.5636796 },
|
| 149 |
+
{ "pais_codigo": "PA", "pais": "Panama", "capital": "Panamá", "latitud": 8.9714493, "longitud": -79.5341802 },
|
| 150 |
+
{ "pais_codigo": "FI", "pais": "Finland", "capital": "Helsinki", "latitud": 60.1666204, "longitud": 24.9435408 },
|
| 151 |
+
{ "pais_codigo": "BT", "pais": "Bhutan", "capital": "Thimphu", "latitud": 27.4713546, "longitud": 89.6336729 },
|
| 152 |
+
{ "pais_codigo": "MT", "pais": "Malta", "capital": "Valletta", "latitud": 35.8989979, "longitud": 14.5136607 },
|
| 153 |
+
{ "pais_codigo": "HR", "pais": "Croatia", "capital": "Zagreb", "latitud": 45.8130967, "longitud": 15.9772795 },
|
| 154 |
+
{ "pais_codigo": "CZ", "pais": "Czechia", "capital": "Prague", "latitud": 50.0874654, "longitud": 14.4212535 },
|
| 155 |
+
{ "pais_codigo": "GE", "pais": "Georgia", "capital": "Tbilisi", "latitud": 41.6934591, "longitud": 44.8014495 },
|
| 156 |
+
{ "pais_codigo": "TH", "pais": "Thailand", "capital": "Bangkok", "latitud": 13.7524938, "longitud": 100.4935089 },
|
| 157 |
+
{ "pais_codigo": "BE", "pais": "Belgium", "capital": "Brussels", "latitud": 50.8467372, "longitud": 4.352493 },
|
| 158 |
+
{ "pais_codigo": "JO", "pais": "Jordania", "capital": "Aman", "latitud": 31.9515694, "longitud": 35.9239625 },
|
| 159 |
+
{ "pais_codigo": "BJ", "pais": "Benin", "capital": "Porto-Novo", "latitud": 6.4990718, "longitud": 2.6253361 },
|
| 160 |
+
{ "pais_codigo": "QA", "pais": "Qatar", "capital": "Doha", "latitud": 25.2856329, "longitud": 51.5264162 },
|
| 161 |
+
{ "pais_codigo": "RU", "pais": "Russia", "capital": "Moscow", "latitud": 55.7505412, "longitud": 37.6174782 },
|
| 162 |
+
{ "pais_codigo": "BN", "pais": "Brunei", "capital": "Bandar Seri Begawan", "latitud": 4.8895453, "longitud": 114.9417574 },
|
| 163 |
+
{ "pais_codigo": "LY", "pais": "Libya", "capital": "Tripoli", "latitud": 32.896672, "longitud": 13.1777923 },
|
| 164 |
+
{ "pais_codigo": "BG", "pais": "Bulgaria", "capital": "Sofia", "latitud": 42.6977028, "longitud": 23.3217359 },
|
| 165 |
+
{ "pais_codigo": "MC", "pais": "Monaco", "capital": "Monaco", "latitud": 43.7310164, "longitud": 7.4209592 },
|
| 166 |
+
{ "pais_codigo": "AL", "pais": "Albania", "capital": "Tirane", "latitud": 41.3281482, "longitud": 19.8184435 },
|
| 167 |
+
{ "pais_codigo": "MY", "pais": "Malaysia", "capital": "Kuala Lumpur", "latitud": 3.1516964, "longitud": 101.6942371 },
|
| 168 |
+
{ "pais_codigo": "CY", "pais": "Cyprus", "capital": "Nicosia", "latitud": 35.1746503, "longitud": 33.3638783 },
|
| 169 |
+
{ "pais_codigo": "KR", "pais": "South Korea", "capital": "Seoul", "latitud": 37.5666791, "longitud": 126.9782914 },
|
| 170 |
+
{ "pais_codigo": "LI", "pais": "Liechtenstein", "capital": "Vaduz", "latitud": 47.1392862, "longitud": 9.5227962 },
|
| 171 |
+
{ "pais_codigo": "BA", "pais": "Bosnia and Herzegovina", "capital": "Sarajevo", "latitud": 43.8570713, "longitud": 18.4126147 },
|
| 172 |
+
{ "pais_codigo": "IS", "pais": "Iceland", "capital": "Reykjavík", "latitud": 64.145981, "longitud": -21.9422367 },
|
| 173 |
+
{ "pais_codigo": "AZ", "pais": "Azerbaijan", "capital": "Baku", "latitud": 40.3755885, "longitud": 49.8328009 },
|
| 174 |
+
{ "pais_codigo": "TG", "pais": "Togo", "capital": "Lomé", "latitud": 6.130419, "longitud": 1.215829 },
|
| 175 |
+
{ "pais_codigo": "IM", "pais": "Isle of Man", "capital": "Douglas", "latitud": 54.149774, "longitud": -4.4779021 },
|
| 176 |
+
{ "pais_codigo": "FJ", "pais": "Fiji", "capital": "Suva", "latitud": -18.1415884, "longitud": 178.4421662 },
|
| 177 |
+
{ "pais_codigo": "TT", "pais": "Trinidad and Tobago", "capital": "Port of Spain", "latitud": 10.6572678, "longitud": -61.5180173 },
|
| 178 |
+
{ "pais_codigo": "XK", "pais": "Kosovo", "capital": "Prishtinë", "latitud": 42.6638771, "longitud": 21.1640849 },
|
| 179 |
+
{ "pais_codigo": "GQ", "pais": "Equatorial Guinea", "capital": "Malabo", "latitud": 1.5994535, "longitud": 10.8268276 },
|
| 180 |
+
{ "pais_codigo": "TV", "pais": "Tuvalu", "capital": "Funafuti", "latitud": -8.5199633, "longitud": 179.1982548 },
|
| 181 |
+
{ "pais_codigo": "MV", "pais": "Maldives", "capital": "Male", "latitud": 4.1779879, "longitud": 73.5107387 },
|
| 182 |
+
{ "pais_codigo": "BD", "pais": "Bangladesh", "capital": "Dhaka", "latitud": 23.7643863, "longitud": 90.3890144 },
|
| 183 |
+
{ "pais_codigo": "IE", "pais": "Ireland", "capital": "Dublin", "latitud": 53.3493795, "longitud": -6.2605593 },
|
| 184 |
+
{ "pais_codigo": "PW", "pais": "Palau", "capital": "Ngerulmud", "latitud": 7.5006446, "longitud": 134.6242864 },
|
| 185 |
+
{ "pais_codigo": "PE", "pais": "Peru", "capital": "Lima", "latitud": -12.0459808, "longitud": -77.0305912 },
|
| 186 |
+
{ "pais_codigo": "LC", "pais": "Saint Lucia", "capital": "Castries", "latitud": 14.0095966, "longitud": -60.9902359 },
|
| 187 |
+
{ "pais_codigo": "DM", "pais": "Dominica", "capital": "Roseau", "latitud": 15.2991923, "longitud": -61.3872868 },
|
| 188 |
+
{ "pais_codigo": "ER", "pais": "Eritrea", "capital": "Asmara", "latitud": 15.3389667, "longitud": 38.9326763 },
|
| 189 |
+
{ "pais_codigo": "MW", "pais": "Malawi", "capital": "Lilongwe", "latitud": -13.9875107, "longitud": 33.768144 },
|
| 190 |
+
{ "pais_codigo": "NI", "pais": "Nicaragua", "capital": "Managua", "latitud": 12.1547116, "longitud": -86.273725 },
|
| 191 |
+
{ "pais_codigo": "SV", "pais": "El Salvador", "capital": "San Salvador", "latitud": 13.697629, "longitud": -89.191156 },
|
| 192 |
+
{ "pais_codigo": "HN", "pais": "Honduras", "capital": "Tegucigalpa", "latitud": 14.1058135, "longitud": -87.2047053 },
|
| 193 |
+
{ "pais_codigo": "CR", "pais": "Costa Rica", "capital": "San Jose", "latitud": 9.9327707, "longitud": -84.0796144 },
|
| 194 |
+
{ "pais_codigo": "BR", "pais": "Brazil", "capital": "Brasilia", "latitud": -15.721487, "longitud": -48.1021719 }
|
| 195 |
+
]
|
frontend/src/map.js
CHANGED
|
@@ -23,31 +23,22 @@
|
|
| 23 |
|
| 24 |
import L from 'leaflet'
|
| 25 |
import 'leaflet/dist/leaflet.css'
|
| 26 |
-
|
| 27 |
-
// Coordenadas aproximadas por countryCode ISO2
|
| 28 |
-
const COORDS = {
|
| 29 |
-
US: [37.09, -95.71],
|
| 30 |
-
DE: [51.16, 10.45],
|
| 31 |
-
GB: [55.37, -3.43],
|
| 32 |
-
BR: [-14.23, -51.92],
|
| 33 |
-
CN: [35.86, 104.19],
|
| 34 |
-
IN: [20.59, 78.96],
|
| 35 |
-
KR: [35.90, 127.76],
|
| 36 |
-
SA: [23.88, 45.07],
|
| 37 |
-
FR: [46.22, 2.21],
|
| 38 |
-
JP: [36.20, 138.25],
|
| 39 |
-
AU: [-25.27, 133.77],
|
| 40 |
-
CA: [56.13, -106.34],
|
| 41 |
-
RU: [61.52, 105.31],
|
| 42 |
-
MX: [23.63, -102.55],
|
| 43 |
-
ZA: [-30.55, 22.93],
|
| 44 |
-
}
|
| 45 |
|
| 46 |
let mapInstance = null
|
| 47 |
-
let bubbles = {} // marketId ->
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
-
|
| 50 |
-
return
|
| 51 |
}
|
| 52 |
|
| 53 |
function getSignalColor(signal) {
|
|
@@ -77,7 +68,7 @@ export function init(containerId, markets, signals, onSelect) {
|
|
| 77 |
worldCopyJump: true,
|
| 78 |
}).setView([25, 10], 2)
|
| 79 |
|
| 80 |
-
//
|
| 81 |
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
| 82 |
attribution: '©OpenStreetMap ©CartoDB',
|
| 83 |
subdomains: 'abcd',
|
|
@@ -87,7 +78,7 @@ export function init(containerId, markets, signals, onSelect) {
|
|
| 87 |
markets.forEach((m) => {
|
| 88 |
const sig = signals.find((s) => s.marketId === m.id) || { signal: 'neutral' }
|
| 89 |
const color = getSignalColor(sig.signal)
|
| 90 |
-
const coords = getCoords(m
|
| 91 |
const radius = getRadius(m.volumeEur)
|
| 92 |
|
| 93 |
const circle = L.circleMarker(coords, {
|
|
@@ -106,7 +97,7 @@ export function init(containerId, markets, signals, onSelect) {
|
|
| 106 |
fillOpacity: 0.8,
|
| 107 |
}).addTo(mapInstance)
|
| 108 |
|
| 109 |
-
//
|
| 110 |
const labelSpan = document.createElement('span')
|
| 111 |
labelSpan.className = 'map-label-text'
|
| 112 |
labelSpan.textContent = m.countryCode || 'GL'
|
|
@@ -121,7 +112,7 @@ export function init(containerId, markets, signals, onSelect) {
|
|
| 121 |
interactive: false,
|
| 122 |
}).addTo(mapInstance)
|
| 123 |
|
| 124 |
-
//
|
| 125 |
const popup = document.createElement('div')
|
| 126 |
popup.className = 'map-popup'
|
| 127 |
const popupCat = document.createElement('div')
|
|
@@ -153,7 +144,7 @@ export function init(containerId, markets, signals, onSelect) {
|
|
| 153 |
export function updateBubble(marketId, newPrice) {
|
| 154 |
const b = bubbles[marketId]
|
| 155 |
if (!b) return
|
| 156 |
-
//
|
| 157 |
const newRadius = b.circle.getRadius() + (Math.random() > 0.5 ? 0.5 : -0.5)
|
| 158 |
b.circle.setRadius(Math.max(5, Math.min(22, newRadius)))
|
| 159 |
}
|
|
|
|
| 23 |
|
| 24 |
import L from 'leaflet'
|
| 25 |
import 'leaflet/dist/leaflet.css'
|
| 26 |
+
import { getCoordsByCode, detectCountryInText } from './capitals.js'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
let mapInstance = null
|
| 29 |
+
let bubbles = {} // marketId -> marcador de circulo
|
| 30 |
+
|
| 31 |
+
function getCoords(market) {
|
| 32 |
+
// 1. intentar por countryCode ISO2 del mercado
|
| 33 |
+
const byCode = getCoordsByCode(market.countryCode)
|
| 34 |
+
if (byCode) return byCode
|
| 35 |
+
|
| 36 |
+
// 2. fallback: detectar país en el texto de la pregunta
|
| 37 |
+
const byText = detectCountryInText(market.question)
|
| 38 |
+
if (byText) return byText
|
| 39 |
|
| 40 |
+
// 3. default centrado en África (coordenadas genéricas)
|
| 41 |
+
return [20, 0]
|
| 42 |
}
|
| 43 |
|
| 44 |
function getSignalColor(signal) {
|
|
|
|
| 68 |
worldCopyJump: true,
|
| 69 |
}).setView([25, 10], 2)
|
| 70 |
|
| 71 |
+
// Capa de tiles oscura (CartoDB Dark Matter)
|
| 72 |
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
| 73 |
attribution: '©OpenStreetMap ©CartoDB',
|
| 74 |
subdomains: 'abcd',
|
|
|
|
| 78 |
markets.forEach((m) => {
|
| 79 |
const sig = signals.find((s) => s.marketId === m.id) || { signal: 'neutral' }
|
| 80 |
const color = getSignalColor(sig.signal)
|
| 81 |
+
const coords = getCoords(m)
|
| 82 |
const radius = getRadius(m.volumeEur)
|
| 83 |
|
| 84 |
const circle = L.circleMarker(coords, {
|
|
|
|
| 97 |
fillOpacity: 0.8,
|
| 98 |
}).addTo(mapInstance)
|
| 99 |
|
| 100 |
+
// Construye texto de etiqueta de forma segura via textContent antes de obtener outerHTML
|
| 101 |
const labelSpan = document.createElement('span')
|
| 102 |
labelSpan.className = 'map-label-text'
|
| 103 |
labelSpan.textContent = m.countryCode || 'GL'
|
|
|
|
| 112 |
interactive: false,
|
| 113 |
}).addTo(mapInstance)
|
| 114 |
|
| 115 |
+
// Construye DOM del popup — textContent previene cualquier inyeccion HTML desde datos del mercado
|
| 116 |
const popup = document.createElement('div')
|
| 117 |
popup.className = 'map-popup'
|
| 118 |
const popupCat = document.createElement('div')
|
|
|
|
| 144 |
export function updateBubble(marketId, newPrice) {
|
| 145 |
const b = bubbles[marketId]
|
| 146 |
if (!b) return
|
| 147 |
+
// Ajusta ligeramente el radio basado en nueva actividad (comportamiento simulado)
|
| 148 |
const newRadius = b.circle.getRadius() + (Math.random() > 0.5 ? 0.5 : -0.5)
|
| 149 |
b.circle.setRadius(Math.max(5, Math.min(22, newRadius)))
|
| 150 |
}
|
frontend/src/simulator.js
CHANGED
|
@@ -3,14 +3,14 @@
|
|
| 3 |
*
|
| 4 |
* Responsabilidades:
|
| 5 |
* - init(state) → recibe referencia al estado global de app.js.
|
| 6 |
-
* - openPosition(...) → crea posicion via API
|
| 7 |
-
* - closePosition(id) → cierra posicion via API
|
| 8 |
*
|
| 9 |
* Flujo openPosition:
|
| 10 |
* 1. Valida cantidad > 0.
|
| 11 |
* 2. Obtiene precio de entrada del mercado actual.
|
| 12 |
* 3. POST /api/v1/positions → si 2xx, actualiza estado.
|
| 13 |
-
* 4. Si API
|
| 14 |
*
|
| 15 |
* Eventos:
|
| 16 |
* - Emite CustomEvent 'positions:changed' para que app.js re-renderice.
|
|
@@ -48,32 +48,20 @@ export async function openPosition(marketId, outcome, amount) {
|
|
| 48 |
try {
|
| 49 |
const created = await api.createPosition(data)
|
| 50 |
appState.positions.push(created)
|
|
|
|
| 51 |
} catch (e) {
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
appState.positions.push({
|
| 55 |
-
id: fakeId,
|
| 56 |
-
marketId,
|
| 57 |
-
outcome,
|
| 58 |
-
amountEur: amt,
|
| 59 |
-
entryPrice,
|
| 60 |
-
currentPrice: entryPrice,
|
| 61 |
-
pnl: 0,
|
| 62 |
-
kellyFraction: 0.2,
|
| 63 |
-
openedAt: new Date().toISOString(),
|
| 64 |
-
})
|
| 65 |
}
|
| 66 |
-
|
| 67 |
-
// Trigger re-render via app.js if needed
|
| 68 |
-
document.dispatchEvent(new CustomEvent('positions:changed'))
|
| 69 |
}
|
| 70 |
|
| 71 |
export async function closePosition(positionId) {
|
| 72 |
try {
|
| 73 |
await api.closePosition(positionId)
|
|
|
|
|
|
|
| 74 |
} catch (e) {
|
| 75 |
-
console.
|
|
|
|
| 76 |
}
|
| 77 |
-
appState.positions = appState.positions.filter((p) => p.id !== positionId)
|
| 78 |
-
document.dispatchEvent(new CustomEvent('positions:changed'))
|
| 79 |
}
|
|
|
|
| 3 |
*
|
| 4 |
* Responsabilidades:
|
| 5 |
* - init(state) → recibe referencia al estado global de app.js.
|
| 6 |
+
* - openPosition(...) → crea posicion via API POST /positions.
|
| 7 |
+
* - closePosition(id) → cierra posicion via API DELETE /positions/:id.
|
| 8 |
*
|
| 9 |
* Flujo openPosition:
|
| 10 |
* 1. Valida cantidad > 0.
|
| 11 |
* 2. Obtiene precio de entrada del mercado actual.
|
| 12 |
* 3. POST /api/v1/positions → si 2xx, actualiza estado.
|
| 13 |
+
* 4. Si API falla → muestra error al usuario.
|
| 14 |
*
|
| 15 |
* Eventos:
|
| 16 |
* - Emite CustomEvent 'positions:changed' para que app.js re-renderice.
|
|
|
|
| 48 |
try {
|
| 49 |
const created = await api.createPosition(data)
|
| 50 |
appState.positions.push(created)
|
| 51 |
+
document.dispatchEvent(new CustomEvent('positions:changed'))
|
| 52 |
} catch (e) {
|
| 53 |
+
console.error('Error abriendo posicion:', e)
|
| 54 |
+
alert('No se pudo abrir la posición. ¿Has iniciado sesión?')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
}
|
|
|
|
|
|
|
|
|
|
| 56 |
}
|
| 57 |
|
| 58 |
export async function closePosition(positionId) {
|
| 59 |
try {
|
| 60 |
await api.closePosition(positionId)
|
| 61 |
+
appState.positions = appState.positions.filter((p) => p.id !== positionId)
|
| 62 |
+
document.dispatchEvent(new CustomEvent('positions:changed'))
|
| 63 |
} catch (e) {
|
| 64 |
+
console.error('Error cerrando posicion:', e)
|
| 65 |
+
alert('No se pudo cerrar la posición')
|
| 66 |
}
|
|
|
|
|
|
|
| 67 |
}
|