mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
- Move GEOPOLITICAL_TAGS, TECH_TAGS, FINANCE_TAGS, and EXCLUDE_KEYWORDS to shared/prediction-tags.json so seed, RPC handler, and client all reference a single source of truth - Remove open_interest proto field (always 0 for Polymarket, never displayed in UI) and corresponding openInterest assignments Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Elie Habib <elie.habib@gmail.com>
83 lines
2.7 KiB
JavaScript
83 lines
2.7 KiB
JavaScript
import predictionTags from '../shared/prediction-tags.json' with { type: 'json' };
|
|
|
|
export const EXCLUDE_KEYWORDS = predictionTags.excludeKeywords;
|
|
|
|
export const MEME_PATTERNS = [
|
|
/\b(lebron|kanye|oprah|swift|rogan|dwayne|kardashian|cardi\s*b)\b/i,
|
|
/\b(alien|ufo|zombie|flat earth)\b/i,
|
|
];
|
|
|
|
export const REGION_PATTERNS = {
|
|
america: /\b(us|u\.s\.|united states|america|trump|biden|congress|federal reserve|canada|mexico|brazil)\b/i,
|
|
eu: /\b(europe|european|eu|nato|germany|france|uk|britain|macron|ecb)\b/i,
|
|
mena: /\b(middle east|iran|iraq|syria|israel|palestine|gaza|saudi|yemen|houthi|lebanon)\b/i,
|
|
asia: /\b(china|japan|korea|india|taiwan|xi jinping|asean)\b/i,
|
|
latam: /\b(latin america|brazil|argentina|venezuela|colombia|chile)\b/i,
|
|
africa: /\b(africa|nigeria|south africa|ethiopia|sahel|kenya)\b/i,
|
|
oceania: /\b(australia|new zealand)\b/i,
|
|
};
|
|
|
|
export function isExcluded(title) {
|
|
const lower = title.toLowerCase();
|
|
return EXCLUDE_KEYWORDS.some(kw => lower.includes(kw));
|
|
}
|
|
|
|
export function isMemeCandidate(title, yesPrice) {
|
|
if (yesPrice >= 15) return false;
|
|
return MEME_PATTERNS.some(p => p.test(title));
|
|
}
|
|
|
|
export function tagRegions(title) {
|
|
return Object.entries(REGION_PATTERNS)
|
|
.filter(([, re]) => re.test(title))
|
|
.map(([region]) => region);
|
|
}
|
|
|
|
export function parseYesPrice(market) {
|
|
try {
|
|
const prices = JSON.parse(market.outcomePrices || '[]');
|
|
if (prices.length >= 1) {
|
|
const p = parseFloat(prices[0]);
|
|
if (!isNaN(p) && p >= 0 && p <= 1) return +(p * 100).toFixed(1);
|
|
}
|
|
} catch {}
|
|
return null;
|
|
}
|
|
|
|
export function shouldInclude(m, relaxed = false) {
|
|
const minPrice = relaxed ? 5 : 10;
|
|
const maxPrice = relaxed ? 95 : 90;
|
|
if (m.yesPrice < minPrice || m.yesPrice > maxPrice) return false;
|
|
if (m.volume < 5000) return false;
|
|
if (isExcluded(m.title)) return false;
|
|
if (isMemeCandidate(m.title, m.yesPrice)) return false;
|
|
return true;
|
|
}
|
|
|
|
export function scoreMarket(m) {
|
|
const uncertainty = 1 - (2 * Math.abs(m.yesPrice - 50) / 100);
|
|
const vol = Math.log10(Math.max(m.volume, 1)) / Math.log10(10_000_000);
|
|
return (uncertainty * 0.6) + (Math.min(vol, 1) * 0.4);
|
|
}
|
|
|
|
export function isExpired(endDate) {
|
|
if (!endDate) return false;
|
|
const ms = Date.parse(endDate);
|
|
return Number.isFinite(ms) && ms < Date.now();
|
|
}
|
|
|
|
export function filterAndScore(candidates, tagFilter, limit = 25) {
|
|
let filtered = candidates.filter(m => !isExpired(m.endDate));
|
|
if (tagFilter) filtered = filtered.filter(tagFilter);
|
|
|
|
let result = filtered.filter(m => shouldInclude(m));
|
|
if (result.length < 15) {
|
|
result = filtered.filter(m => shouldInclude(m, true));
|
|
}
|
|
|
|
return result
|
|
.map(m => ({ ...m, regions: tagRegions(m.title) }))
|
|
.sort((a, b) => scoreMarket(b) - scoreMarket(a))
|
|
.slice(0, limit);
|
|
}
|