An AI Economic Scenario Observatory that keeps observed data, scenario assumptions, and model prose in three visibly separate lanes.
MacroScope is a localhost web application for policy teams, economic researchers, risk analysts, and finance practitioners who need to turn country indicators into an auditable scenario briefing. It loads the World Bank Indicators API without a key, accepts CSV/JSON when official access is unavailable or the analyst has better data, measures trend/volatility/gaps/regime, propagates five macro shocks through a versioned graph, and exports both a readable briefing draft and a machine-readable decision record.
AI is optional and structurally bounded: it may extract policy text or explain saved result IDs. It does not calculate the country indicators, regime, shock outcomes, uncertainty bands, or assumption flips. A server-side grounding check withholds AI output that introduces unsupported numbers or evidence IDs.
Method boundary: MacroScope is a scenario observatory, not an econometric forecast, investment signal, or policy recommendation engine. Every graph coefficient is an illustrative, unvalidated editorial assumption with per-edge provenance metadata—not a fitted causal estimate. P10/P90 are bounded assumption-propagation intervals, not empirical probabilities.
Real local app screenshot · Germany fixture · graph 2026.1 · Offline Mock available but not needed for these numbers
Real local-browser workflow · no external AI provider required
- The problem
- What MacroScope does
- Why this is useful
- Five-minute demo
- Quick start
- Input paths
- How it works
- AI and BYOK boundary
- Task resilience
- Exports
- HTTP API
- Project structure
- Tests
- Security and privacy
- Limitations
- Roadmap
- License
Country-macro work often breaks its evidence chain in one of three places:
- an indicator dashboard shows the latest value but hides missing years and vintage mismatch;
- a scenario spreadsheet mixes observed values with coefficients and subjective assumptions;
- an AI-generated memo sounds coherent while quietly adding a number that never existed in the source.
MacroScope preserves those boundaries in the interface and in every export:
| Layer | What belongs there | What does not belong there |
|---|---|---|
| Observation | Source values, trend slope, change volatility, gaps, regime evidence IDs | Interpolated missing years or model prose |
| Scenario | Named shock inputs, graph version/fingerprint, coefficient ranges, bounded outcomes, flip points | Claims that the interval is a probability forecast |
| AI annotation | Policy-text extraction and plain-language explanation of saved IDs | New indicators, recalculation, unsupported numbers, trading advice |
| Decision record | Human owner, review date, chosen next action, provenance, model limits | Automatic approval or recommendation |
| Capability | Generic indicator dashboard | Spreadsheet scenario | Unbounded AI chat | MacroScope |
|---|---|---|---|---|
| No-key official country ingest | Sometimes | No | No | Yes |
| CSV and JSON fallback | Rarely | Yes | Manual | Yes |
| Explicit gap ledger; no interpolation | Rarely | Manual | No | Yes |
| Versioned shock graph + fingerprint | No | Manual | No | Yes |
| Reproducible bounded uncertainty | No | Varies | No | Yes |
| Assumption-flip thresholds | No | Manual | Usually prose | Yes |
| Numeric grounding for AI output | N/A | N/A | Usually no | Yes |
| Key-free local history and resume | No | File-based | Varies | Yes |
| Briefing + decision-record export | Dashboard only | Manual | Prose only | Yes |
MacroScope converts an official or imported country-indicator station into an auditable observation, a versioned scenario bulletin, assumption-flip thresholds, and a human-owned decision record. AI remains a bounded annotation layer.
- Country station ingestion: fetches nine World Bank indicator families concurrently without an API key, while retaining URL, indicator code, and retrieval time in the source log.
- Real fallback path: imports wide CSV, long CSV, canonical JSON, indicator maps, and compact year maps. Six realistic country fixtures plus one malformed edge case run offline.
- Economic weather observatory: calculates least-squares trend, sample volatility of annual changes, latest/previous values, gap counts, coverage, core-series sufficiency, and a transparent monitoring regime.
- Five-front shock graph: propagates energy, policy-rate, external-demand, FX, and fiscal shocks into GDP growth, inflation, and current-account outcomes.
- Illustrative graph provenance: ships
2025.4and2026.1scenario-assumption vintages. Each edge records an assumption ID, unit, horizon, sign rationale, review date, and source-status note; each run stores full ranges and a stable SHA-256-derived fingerprint. The2026.1fingerprint isc671234d91816130. Traceability does not economically validate the coefficient. - Bounded uncertainty: draws reproducible triangular paths from the user input band and each edge's low/central/high coefficient. The UI and export label the result as an assumption interval.
- Assumption flips: solves the shock level that would move an outcome across a stated watch threshold while other central contributions stay fixed.
- Grounded AI desk: supports policy extraction and deterministic-result explanation through Offline Mock, OpenAI, Anthropic, Gemini, DeepSeek, Moonshot Kimi, Zhipu GLM, or a custom endpoint.
- Operational handoff: exports a Markdown briefing, JSON decision record, or CSV outcome table with source log, quality label, graph fingerprint, assumptions, interval, flips, and limits.
The product is designed for a real meeting sequence rather than a one-off chart:
- an analyst opens an official or internal country station;
- the observatory exposes stale series and internal gaps before anyone discusses a scenario;
- the team selects a graph vintage and moves five explicit shock fronts;
- the nearest assumption flips reveal which exposure deserves primary-source verification;
- AI can extract the communiqué or explain saved result IDs, but a numeric whitelist blocks unsupported prose;
- a named human saves the next action and review date into a reusable decision record.
Removing AI still leaves the full numeric observatory, scenario, flip analysis, and exports. Losing official API access still leaves CSV/JSON ingestion and the offline fixture set. The cooperating subsystems produce useful partial value instead of collapsing around one mechanism.
- Start the server and open
http://127.0.0.1:8000. - Under Station intake, choose
Germany / energy squeezeand click Open. - Inspect the observed regime, indicator traces, coverage, and missing-weather ledger.
- Open Shock map. Keep graph
2026.1, set an energy shock, rate move, and external-demand shock, then issue the bulletin. - Inspect GDP/inflation/current-account intervals, edge contributions, and nearest assumption flips.
- Open Policy desk, keep Offline Mock, paste a policy statement, and run extraction or explanation.
- Open Briefing room, add an owner/review date, then export Markdown, JSON, or CSV.
- Refresh the page, open Archive, and resume the checkpoint. The provider key will not be restored because it was never written there.
- Country-risk and policy-monitoring meeting preparation.
- Economic scenario exercises with visible coefficient and data-vintage assumptions.
- Teaching trend, volatility, missingness, conditional scenarios, and uncertainty boundaries.
- Producing a source-labeled briefing draft that a human analyst can review and own.
Bundled fixtures:
| Fixture | Shape | Purpose |
|---|---|---|
argentina_fx_stress.csv |
wide CSV | high inflation, FX depreciation, fiscal/external strain |
germany_energy_squeeze.csv |
wide CSV | energy exposure and weak growth |
kenya_gap_case.csv |
sparse wide CSV | missing years and uneven series coverage |
china_demand_transition.json |
indicator map | multi-series demand transition |
indonesia_resilient_mix.json |
long records | negative/no-urgent-issue style case with mixed structure |
united_states_tightening.json |
nested series | rate and fiscal/current-account watch |
malformed_bad_year.csv |
malformed CSV | responsible import rejection |
- Python 3.8 or newer
- A modern browser
- No package installation, database server, Node.js, or API key
git clone https://github.com/ZhenyuanPAN822/macroscope.git
cd macroscope
python server.pyExpected output:
MacroScope / Economic Weather Bureau
serving ...\repo\web
open http://127.0.0.1:8000/
Ctrl-C to stop
Choose a different port if 8000 is already occupied:
python server.py --port 8049The durable, key-free SQLite history defaults to ~/.macroscope/tasks.sqlite3. Override it without changing source code:
$env:MACROSCOPE_DATA_DIR = "D:\private\macroscope-data"
python server.pyEnter an ISO-style country code and date range. MacroScope requests these canonical series when available:
| MacroScope key | World Bank code | Display unit |
|---|---|---|
gdp_growth |
NY.GDP.MKTP.KD.ZG |
% y/y |
inflation |
FP.CPI.TOTL.ZG |
% y/y |
unemployment |
SL.UEM.TOTL.ZS |
% labor force |
policy_rate |
FR.INR.RINR |
real interest-rate proxy, % |
fx_rate |
PA.NUS.FCRF |
local currency per USD |
fiscal_balance |
GC.BAL.CASH.GD.ZS |
% GDP |
exports_growth |
NE.EXP.GNFS.KD.ZG |
% y/y |
current_account |
BN.CAB.XOKA.GD.ZS |
% GDP |
energy_imports |
EG.IMP.CONS.ZS |
% energy use |
The API path is partial by design: missing or unavailable series stay in source_errors and reduce the data-quality label. MacroScope never fills a gap with an invented value.
country_code,country_name,year,gdp_growth,inflation,unemployment,policy_rate,fx_rate
DEU,Germany,2023,-0.3,5.9,3.1,0.6,0.92
DEU,Germany,2024,-0.2,2.3,3.4,1.4,0.92country_code,country_name,year,indicator,value
IDN,Indonesia,2024,gdp_growth,5.0
IDN,Indonesia,2024,inflation,2.3{
"country": {"code": "CHN", "name": "China"},
"indicators": {
"gdp_growth": [[2022, 3.0], [2023, 5.2], [2024, 5.0]],
"inflation": [[2022, 2.0], [2023, 0.2], [2024, 0.2]]
}
}Aliases and World Bank codes are accepted. Unknown columns are ignored; invalid years and non-finite values are rejected. Duplicate country-year-indicator observations use the last value and create a quality warning.
flowchart LR
A[World Bank API] --> C[Canonical country dataset]
B[CSV / JSON fallback] --> C
C --> D[Quality + gap ledger]
C --> E[Trend + volatility]
D --> F[Observed regime]
E --> F
F --> G[Versioned shock graph]
G --> H[Bounded intervals]
G --> I[Assumption flips]
J[Policy text] --> K[Bounded AI desk]
H --> K
I --> K
K --> L[Grounding validator]
H --> M[Briefing + decision record]
I --> M
L --> M
M --> N[(Key-free SQLite history)]
- Trend: ordinary least-squares slope over the latest configurable window (default eight available observations).
- Volatility: sample standard deviation of annualized changes; gaps longer than one year are divided by elapsed years rather than mislabeled year-over-year moves.
- Gaps: missing integer years between the dataset's minimum and maximum year. No interpolation.
- Data readiness: a completeness/freshness grade using the requested-series denominator, core-series presence, source failures, configurable as-of year, absolute staleness, and vintage alignment. It is not a confidence score about the economy or scenario.
- Regime: transparent thresholds across growth, price, financial, external, and fiscal pressure. Each rationale carries an
OBS.*evidence ID.
Raw shock inputs are normalized into domain units—for example, 100 basis points for the policy-rate front and 10% for energy/FX. Each graph edge stores central, low, and high coefficients, lag label, transmission channel, and explicit illustrative-assumption provenance. A seeded simulation draws triangular values from the input band and coefficient ranges. It does not fit coefficients to the loaded country history. GDP, inflation, and current-account baselines retain their own years; excessive vintage gaps are surfaced rather than silently blended.
An assumption flip solves the driver value needed to cross a watch threshold while holding other central contributions fixed. This makes the model challengeable: the user can see which assumption would reverse the status and verify that exposure outside MacroScope.
| Provider | Adapter | Default endpoint family |
|---|---|---|
| Offline Mock | local normalized response | mock://offline |
| OpenAI | OpenAI-compatible chat completions | https://api.openai.com/v1 |
| Anthropic | native Messages API | https://api.anthropic.com/v1 |
| Google Gemini | native generateContent |
https://generativelanguage.googleapis.com/v1beta |
| DeepSeek | OpenAI-compatible | https://api.deepseek.com/v1 |
| Moonshot Kimi | OpenAI-compatible | https://api.moonshot.cn/v1 |
| Zhipu GLM | OpenAI-compatible | https://open.bigmodel.cn/api/paas/v4 |
| Custom / relay | chosen adapter | user-supplied base URL + model |
Provider defaults are editable because model names and endpoints evolve. Test key requires an adapter-specific probe marker instead of accepting any HTTP 200 object, and surfaces invalid-key, missing-model, balance/quota, rate-limit, timeout, network, bad-response, and upstream errors. Provider responses are capped at 2 MiB; redirects are disabled; DNS is resolved and pinned while TLS verifies the original hostname.
- Extract mode: only the pasted policy text and a strict schema requiring measures, dates, institutions, direct quotes, ambiguities, and confidence. Every
direct_quotemust be an exact normalized substring of that policy text. - Explain mode: a compact deterministic bundle containing source-backed observations, regime evidence, graph version/fingerprint, shocks, outcomes, intervals, and flip IDs; optional policy text is clearly separated.
The server validates mode-specific JSON schemas, verifies direct quotes / quoted evidence against source text, extracts numeric tokens and OBS.* / SERIES.* / SCENARIO.* / EDGE.* / FLIP.* IDs, and compares them with the supplied sources. Unsupported output returns HTTP 422 and is not displayed as a valid annotation. This is a strict provenance guard, not a proof that every sentence is economically correct.
Long and repeated work is represented as a local task state machine:
DRAFT → INGESTING → DATA_READY → OBSERVING → OBSERVATORY_READY
→ SIMULATING → SCENARIO_READY → AI_PENDING → COMPLETE
Interrupted provider work can become AI_INTERRUPTED, a grounding rejection becomes PARTIAL, and a user cancellation becomes CANCELLED. The interface shows phase and progress, uses AbortController to stop waiting, preserves completed stages, and lets the user resume a saved bundle from Archive after refresh.
Task history stores sanitized country data, assumptions, deterministic results, AI annotations, token usage, progress, and errors. The recursive sanitizer removes key-like fields before SQLite persistence.
| Export | File | Use |
|---|---|---|
| Briefing draft | macroscope-briefing-draft.md |
readable meeting brief with evidence IDs and method limits |
| Decision record | macroscope-decision-record.json |
auditable archive or downstream workflow input |
| Outcome table | macroscope-outcomes.csv |
portable baseline/central/P10/P90/threshold table |
The decision record includes the human status, owner, review date, chosen next action, country/source log, data readiness (completeness, freshness, as-of date, and source failures), regime, illustrative graph version/fingerprint/provenance, shocks, each outcome's baseline year and vintage gap, bounded outcomes, flip points, alerts, optional AI annotations, token ledger, and consolidated limitations.
See the checked-in example briefing and example decision record for the Germany fixture.
| Method | Path | Purpose |
|---|---|---|
GET |
/api/health |
local readiness and AI/numeric boundary |
GET |
/api/session |
per-process CSRF token and network-mode metadata |
GET |
/api/catalog |
indicator metadata and default graph |
GET |
/api/graph?version=2026.1 |
inspect a graph vintage |
GET |
/api/world-bank/country/CHN?start=2015&end=2024 |
no-key official country ingest |
POST |
/api/ingest |
normalize CSV/JSON fallback |
POST |
/api/observe |
trends, volatility, gaps, quality, regime |
POST |
/api/scenario |
graph propagation, intervals, flips |
POST |
/api/validate |
provider/model test |
POST |
/api/ai |
grounded extraction or explanation |
POST |
/api/export |
Markdown, JSON, or CSV artifact |
GET/POST/PUT/DELETE |
/api/tasks[/<id>] |
history, progress, resume, cancel, cleanup |
All endpoints are same-origin and localhost-oriented. Request bodies are size-limited to 8 MiB. Static serving blocks path traversal.
repo/
├── app/
│ ├── http_app.py # stdlib HTTP API and static server
│ ├── world_bank.py # no-key official data client + partial failure ledger
│ ├── provider_gateway.py # OpenAI/Anthropic/Gemini adapters
│ ├── security.py # URL safety, key validation, redaction
│ ├── mock_provider.py # offline extraction/explanation demo
│ └── task_store.py # key-free SQLite checkpoints
├── domain/
│ ├── schema.py # canonical indicators and aliases
│ ├── ingest.py # CSV/JSON normalization and quality
│ ├── observatory.py # trends, volatility, gaps, regime
│ ├── transmission.py # versioned graph, intervals, flip points
│ ├── policy_ai.py # AI prompts and numeric grounding
│ └── exports.py # briefing and decision artifacts
├── fixtures/ # six country stations + malformed edge case
├── web/ # warm editorial weather-bureau UI
├── tests/ # 30 unit, integrity, security, and integration tests
├── scripts/smoke_test.py # offline end-to-end smoke test
└── server.py # Python 3.8-compatible entry point
Run the full suite:
python -m unittest discover -s tests -vRun the offline end-to-end smoke test:
python scripts/smoke_test.pyCompile every Python module under Python 3.8:
python -m compileall -q app domain tests scripts server.pyThe 30 tests cover CSV/JSON import, multi-country rejection, missing-year and annualized-gap handling, readiness/freshness/as-of logic, malformed/zero/unknown-version/out-of-range inputs, baseline years and vintage gaps, illustrative graph fingerprints/provenance, reproducible uncertainty, assumption flips, strict AI schemas and quote substrings, secret allowlisting, Host/Origin/CSRF/network-mode boundaries, DNS-pinned no-redirect provider calls, response caps, CAS cancellation, archive hydration, World Bank normalization, the HTTP pipeline, offline AI, and exports.
- API keys are entered as masked browser fields and held in memory by default.
- API keys are page-memory only and clear on reload. Optional tab-session persistence stores provider/protocol/base URL/model metadata in
sessionStorage, never the key;localStorageis not used. - The backend receives the key only for the selected provider request and never writes it to SQLite or exports.
- Request bodies and headers are never logged.
- Remote custom endpoints require HTTPS; loopback HTTP is allowed for local relays. DNS is pinned, private/link-local/metadata targets are rejected, redirects are disabled, and responses are capped.
- Provider error text is length-limited and credential-like strings are redacted.
- Local writes require the random token from
/api/session,X-MacroScope-CSRF, a valid loopback Host, same-origin request context, and JSON where applicable. Non-loopback binding is rejected unless--unsafe-networkand an authentication token of at least 16 characters are supplied. - A same-origin Content Security Policy blocks external scripts, object embedding, framing, and cross-origin connections.
- The app is local-first, but a real provider call sends the bounded prompt and policy text to that provider. Review the provider's data policy before using sensitive text.
Explicit network-mode startup:
python server.py --host 0.0.0.0 --unsafe-network --auth-token "replace-with-at-least-16-characters"- World Bank series have country-specific lags, definitions, revisions, and gaps. The real-interest-rate series is a policy/financial-conditions proxy, not a harmonized central-bank policy-rate feed.
- Regime thresholds are transparent monitoring rules, not estimated recession or crisis probabilities.
- Shock coefficients are versioned scenario assumptions shipped with the repository. They are not country-calibrated causal estimates.
- Bounded P10/P90 output reflects selected input and coefficient ranges; it is not a confidence interval derived from historical forecast errors.
- Assumption flips hold other central contributions fixed and do not model nonlinear feedback, time-varying parameters, or endogenous policy response.
- Numeric grounding blocks new numeric tokens and evidence IDs; it does not independently fact-check qualitative model prose.
- Client cancellation stops the browser wait and marks the checkpoint cancelled. A request already received by an external provider may still finish upstream, but its response is discarded if the task remains cancelled.
- The server is designed for one trusted local user, not as a hardened multi-tenant internet service.
- User-editable graph versions with signed manifests and diff view
- Country-specific coefficient packs backed by cited research
- Quarterly/monthly frequency adapters and revision-vintage comparison
- Multi-country spillover layer and trade-weighted external-demand fronts
- Optional time-series forecast evaluation with proper backtesting
- Signed decision-record bundles and organization review workflow
MIT. See LICENSE.
Observe first. Disturb assumptions second. Let AI write last.
