Go to file
Amir Alexander Abdelbaki c3a266f6d9 Backfill a week of market history on first startup; UI polish
Backend: backfill_market_history() fetches 7 days of hourly price bars per
instrument on first container start (skipped once an instrument has more
than a day of real history, so restarts don't re-run it) and runs the same
spike-detection logic retroactively, so the Economic Incident History view
is already populated instead of empty for a week. Runs as a genuine
one-time APScheduler job, separate from the recurring polls.

Also: FROZEN/ERROR overlay badges on market ticker items (MOEX's known-stale
Yahoo feed, and any instrument whose last poll actually failed), each
labeled with the instrument name; a big magenta date/time heading per
economic incident; and the incident article list is now a collapsed
fold-out (some incidents merge to 60 stories) instead of always fully
expanded.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Um48tTvZDrEgDeweFyhPYC
2026-07-20 15:44:53 +02:00
backend Backfill a week of market history on first startup; UI polish 2026-07-20 15:44:53 +02:00
deploy/lxc Add flights overlay, globe icon sprites, and economic incident history 2026-07-20 14:31:21 +02:00
frontend Backfill a week of market history on first startup; UI polish 2026-07-20 15:44:53 +02:00
nginx Initial commit: NewsAtlas news globe 2026-07-20 13:49:00 +02:00
.env.example Volatility-adaptive spike detection + multi-instrument overlay charts 2026-07-20 15:21:37 +02:00
.gitignore Add flights overlay, globe icon sprites, and economic incident history 2026-07-20 14:31:21 +02:00
README.md Backfill a week of market history on first startup; UI polish 2026-07-20 15:44:53 +02:00
docker-compose.yml Add flights overlay, globe icon sprites, and economic incident history 2026-07-20 14:31:21 +02:00

README.md

NewsAtlas

A self-hosted, nginx-fronted "news globe": pulls RSS from a deliberately ideologically-mixed set of outlets, localizes each story onto a 3D globe, merges same-place/same-topic stories into a single expandable point, tracks major stock indices and oil prices over time, and overlays weather and (where available) conflict-event data. Any place name or article text can be looked up on Wikipedia, and country panels show that country's parliament composition diagram when Wikipedia has one.

Layout

  • Left rail — latest news, newest first, auto-refreshing. A 📍 button on any geocoded story flies the globe to it.
  • Right rail — index/oil ticker with a 7-day sparkline per instrument. Click one to expand its recent-spikes-with-candidate-articles preview (see "Economic Incident History" below for the full log).
  • Center — the globe. News clusters render as newspaper icons (tinted violet→pink by corroborating-source count); conflict events render as magenta helmet icons plus a pulsing ring. Clicking a news icon opens a modal with its stories, a Wikipedia summary of the place, and (if available) its country's parliament composition diagram. Text selected anywhere can be searched on Wikipedia via the popup that appears.
  • Top-left "i" button — opens the Economic Incident History view: every detected market spike, full clock-hour window, with the complete list of articles published in it (not just the top few) — a comprehensive log, distinct from the quick preview in the right rail. Spikes across instruments within an hour of each other are merged into one incident (e.g. WTI and Brent crude spiking together shows as one entry, not two — a timestamp heuristic, not evidence the moves actually share a cause). Each incident also shows its likely factors: the words that recur most across every headline published in that window, stopwords filtered out — plain word-frequency counting, no AI/LLM involved (textutil.py). Read it as "worth checking these articles," not an explanation. A "Show comparison charts" button per incident (lazy-loaded) plots two overlaid line charts, each instrument normalized to % change from the window's start so wildly different price scales/currencies are comparable: one with every tracked instrument, one with just the incident's own — useful for checking whether a move was isolated to a couple of correlated instruments or part of a broader swing, and for spotting counter-reactions (something moving the opposite way at the same time). Hover for a hairline crosshair with a per-instrument reading at that moment.
  • Bottom drawer — collapsible conflict/military-event log (ACLED-backed; see below).
  • Flights toggle — live global air traffic (OpenSky Network) as airplane icons: magenta for flagged-military aircraft, the same dark violet as the globe surface for everything else. A "Military only" checkbox hides civilian traffic. Hover an aircraft for callsign/country/altitude/speed.

Globe points use zoom-adaptive clustering: the backend groups articles onto a fixed ~11km grid, then the frontend progressively folds nearby grid cells into a single point as you zoom out (regroupForAltitude in frontend/js/app.js), so a continent-level view doesn't leave dozens of separate dots. The fold radius grows with camera altitude, capped at subcontinent scale — tune it via mergeRadiusDeg() if you want tighter or looser regional grouping.

Color theme is the user's own "CyberQueer" palette (near-black base, hot pink #E40046 + electric violet #5018DD accents), including a tinted globe material — see frontend/css/style.css :root variables to swap it.

National borders render as a bright violet stroke over a transparent fill (Natural Earth admin-0 polygons, loaded from three-globe's own npm package via unpkg — no key or backend endpoint needed).

Each market instrument in the right rail shows a 7-day sparkline (line + area, hover for a crosshair/tooltip) built from /api/markets/history. Article listings (news feed, cluster modal, spike candidate articles) show a small favicon next to each headline, resolved from the article's own URL — decorative only, not a claim of any publisher's official branding.

Stack

  • backend/ — FastAPI + SQLite + APScheduler. Polls RSS feeds, geocodes articles against a curated gazetteer, clusters them, polls Yahoo Finance for markets/oil, optionally polls ACLED for conflict events, and proxies Wikipedia + OpenWeatherMap so API keys never reach the browser.
  • frontend/ — static HTML/CSS/JS using globe.gl (three.js) for the 3D globe. No build step.
  • nginx/ — reverse proxy (/api/* → backend) + static file server, with a short-lived cache in front of the API so many browser tabs don't hammer SQLite.

Running it

cp .env.example .env   # fill in optional API keys, see below
docker compose up --build

Then open http://localhost:8080 (or whatever HTTP_PORT you set).

On first boot the globe will be empty for a few seconds until the first RSS poll completes — hit "Refresh now" if you don't want to wait for the 10-minute interval.

The Economic Incident History, on the other hand, doesn't start empty: on first startup the backend backfills a week of hourly price history per instrument (backfill_market_history() in markets.py) and runs spike detection retroactively across it, so there's already a populated week of incidents rather than a week-long wait for live polling to accumulate enough data. It's a one-time thing — skipped on later restarts once an instrument has more than a day of real history. Backfilled incidents will generally have few or no candidate articles, since RSS feeds only carry recent items and there's no way to backfill a week of news to match a week of backfilled prices — the price/incident data itself is real, the article correlation just can't reach that far back.

Optional API keys (.env)

Everything works with .env left blank except that the weather and conflict overlays disable themselves. Nothing else requires a key.

Var What it unlocks Get one
OWM_API_KEY Weather overlay (clouds/precipitation/temp/wind/pressure) free, openweathermap.org
ACLED_API_KEY + ACLED_EMAIL Conflict/"military movement" event overlay free for registered (incl. non-commercial/research) use, acleddata.com
OPENSKY_USERNAME + OPENSKY_PASSWORD Higher-frequency flights overlay polling optional, free — anonymous access already works, see below

Why ACLED for "military movements": there is no single free, public, real-time feed of military movements. ACLED is the closest widely-used open dataset of sourced, georeferenced conflict/political-violence events and is what the conflict overlay is built on. Treat it as "reported conflict events," not live troop tracking.

Flights overlay ("Flights" toggle) plots live airborne traffic from the OpenSky Network REST API — works anonymously out of the box. Anonymous access is capped at 400 credits/day, so the default poll interval (FLIGHTS_POLL_SECONDS=300) is conservative; register for free and set OPENSKY_USERNAME/OPENSKY_PASSWORD for a 4000/day cap and you can safely poll more often. Aircraft are colored gray (civilian) or red (flagged military) — see the heuristic caveat below before reading too much into the red dots.

What's a heuristic, not ground truth

  • Geocoding (backend/app/geocode.py, data/gazetteer.csv) is keyword matching against a curated ~180-place gazetteer, not full NLP/NER. It will miss unlisted places and can mismatch generic names. Add rows to the CSV to extend coverage.
  • Clustering groups articles whose matched location falls in the same ~11km grid cell within the last ARTICLE_WINDOW_HOURS (default 72h). The "topic" label shown per cluster is just the most frequent significant words across its headlines, not topic modeling.
  • Weather overlay stitches a handful of OpenWeatherMap Web-Mercator tiles into one texture and maps it onto the globe's equirectangular UVs. It's visually indicative (clouds/precip patterns are recognizable) but not pixel-accurate, especially near the poles.
  • Parliament diagrams (backend/app/wikipedia.py, data/parliaments.yaml) reuse the infobox image of each country's legislature article, which is usually — not always — the semicircle/ hemicycle composition chart. Countries without a curated mapping are resolved by a live Wikipedia search at request time, so accuracy varies; fix a bad match by adding an override to parliaments.yaml.
  • MOEX (Russia) index data from Yahoo Finance appears frozen at mid-2022 values — Western data providers largely cut off live Russian market feeds after sanctions. It's shown for continuity, not as a live price.
  • RSS feed URLs (backend/app/sources.yaml) belong to third parties and can change without notice — if a source goes quiet, check its site for a current feed URL and update the file (no rebuild needed, it's a read-only mount).
  • Military flight classification (backend/app/flights.py, data/military_ranges.yaml) is a heuristic, not an authoritative flag — there's no public "is this military" field on live ADS-B data. It combines an ICAO24 hex-address prefix (only the US block is backed by a citable source: OpenSky Network's own 2017 research paper) and a couple of well-known military callsign prefixes (RCH, NATO). This reliably catches US/NATO transport and tanker traffic — the bulk of what's actually visible on public ADS-B — and under-flags everyone else. Extend military_ranges.yaml if you can source ranges/callsigns for other air forces with similar confidence; don't guess, a wrong range misclassifies real civilian flights.

API surface (consumed by the frontend, but usable standalone)

  • GET /api/clusters — merged globe points
  • GET /api/clusters/{cluster_key}/articles — articles behind one point
  • GET /api/articles?q=&limit= — raw article search
  • POST /api/refresh — force an immediate RSS poll
  • GET /api/markets/latest, GET /api/markets/history?symbol=^GSPC (accepts either hours= or an explicit start=&end= ISO-datetime window — the latter is what the incident overlay charts use)
  • GET /api/markets/spikes?symbol=&hours= — sudden index/oil moves, each paired with every article published in a full-clock-hour window around it (MARKET_SPIKE_LOOKBACK_HOURS sets the minimum lookback; the window then snaps outward to whole hours). A move must clear an absolute floor (MARKET_SPIKE_THRESHOLD_PCT) and, once there's enough price history, be at least MARKET_SPIKE_VOLATILITY_MULTIPLIER× that instrument's own recent typical move (MARKET_SPIKE_HISTORY_DAYS lookback, MARKET_SPIKE_MIN_SAMPLES minimum observations before it applies) — so a routinely volatile instrument needs a bigger move to register than a normally-calm one. Candidate articles are ranked by relevance (keyword/country matching) but not filtered by it — timing alone qualifies a story. Click a ticker item for a quick preview, or the "i" button (top-left) for the full log. Heuristic correlation, not a verified causal link.
  • GET /api/markets/incidents?symbol=&hours= — the same spikes merged across instruments within INCIDENT_MERGE_HOURS (1h, markets.py) of each other, each with a top_keywords word-frequency list. Backs the Economic Incident History view.
  • GET /api/conflict-events?hours=168
  • GET /api/flights — latest global OpenSky snapshot, with is_military per aircraft (see the heuristic caveat above)
  • GET /api/weather/tiles/{layer}/{z}/{x}/{y}.png — OWM tile proxy
  • GET /api/wikipedia/summary?title=, GET /api/wikipedia/search?q=, GET /api/wikipedia/parliament?country=

Extending

  • Add/edit RSS sources: backend/app/sources.yaml.
  • Add gazetteer locations: backend/app/data/gazetteer.csv.
  • Add/fix parliament page mappings: backend/app/data/parliaments.yaml.
  • Add/fix military ICAO24/callsign ranges: backend/app/data/military_ranges.yaml.
  • Extend stopwords for keyword extraction (cluster topics, incident "likely factors"): backend/app/textutil.py.
  • Change poll intervals / clustering window: .env.

New model fields need a matching entry in _COLUMN_MIGRATIONS in backend/app/db.pycreate_all() only creates missing tables, not columns on tables that already exist in a deployed data/newsatlas.db.

Deploying as a Proxmox LXC

See deploy/lxc/README.md for a pct-based build script that provisions a Debian/Ubuntu/Alpine LXC container running the full stack, and packages it as a reusable .tar.zst template.