feat: phase 14 — BTC technicals, central bank rates, 3 datasets, dashboard UX (+5 = 84 tools)
New tools: - intel_btc_technicals: SMA-50/200, Mayer Multiple, golden/death cross, ATH distance - intel_central_bank_rates: 15 banks (Fed/ECB/BoE via FRED, 12 curated fallback) - intel_trade_routes: 19 maritime chokepoints with oil flow and vessel transit data - intel_cloud_regions: 28 AWS/Azure/GCP regions with coordinates - intel_financial_centers: GFCI top 20 with rankings and specializations Dashboard additions: - BTC Technicals drawer section (price, SMA, Mayer, cross signal) - Central Bank Rates drawer section (15 banks, sorted by rate) - Trade route markers on infrastructure map layer (chokepoint/canal/route icons) - Regional presets (Globe/Americas/Europe/MENA/Asia-Pac/Africa) with localStorage persistence - Time window filter (1h/6h/24h/7d/All) filtering earthquakes and news by timestamp - Static datasets served via /api/static for instant boot Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## What This Is
|
||||
|
||||
World Intelligence MCP Server — 80 tools across 30 domains providing real-time global intelligence from free public APIs. Serves three interfaces: MCP stdio (for Claude Code/Cursor), a live Starlette dashboard with SSE, and a Click CLI with Rich output. Python 3.11+, built with hatchling.
|
||||
World Intelligence MCP Server — 84 tools across 30+ domains providing real-time global intelligence from free public APIs. Serves three interfaces: MCP stdio (for Claude Code/Cursor), a live Starlette dashboard with SSE, and a Click CLI with Rich output. Python 3.11+, built with hatchling.
|
||||
|
||||
## Commands
|
||||
|
||||
|
||||
+20
-8
@@ -2,7 +2,7 @@
|
||||
|
||||
**Benchmark**: [koala73/worldmonitor](https://github.com/koala73/worldmonitor)
|
||||
**Updated**: 2026-02-26
|
||||
**Current tools**: 80 (79 intel + 1 status)
|
||||
**Current tools**: 84 (83 intel + 1 status)
|
||||
|
||||
---
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
## 1. Data Sources — Complete Inventory
|
||||
|
||||
### Markets & Economics (11 tools)
|
||||
### Markets & Economics (13 tools)
|
||||
| Tool | WM Equivalent | Status |
|
||||
|------|---------------|--------|
|
||||
| `intel_market_quotes` | `list-market-quotes` | :white_check_mark: |
|
||||
@@ -32,6 +32,8 @@
|
||||
| `intel_fred_series` | `get-fred-series` | :white_check_mark: |
|
||||
| `intel_world_bank_indicators` | `list-world-bank-indicators` | :white_check_mark: |
|
||||
| `intel_country_stocks` | Country main index ticker | :white_check_mark: |
|
||||
| `intel_btc_technicals` | BTC SMA-50/200, Mayer Multiple, cross signals | :white_check_mark: |
|
||||
| `intel_central_bank_rates` | 15 central bank policy rates (FRED + curated) | :white_check_mark: |
|
||||
|
||||
### Natural Disasters & Climate (5 tools)
|
||||
| Tool | WM Equivalent | Status |
|
||||
@@ -176,6 +178,9 @@
|
||||
| Spaceports | 27 launch facilities | `intel_spaceports` | :white_check_mark: |
|
||||
| Critical minerals | 27 deposit types | `intel_critical_minerals` | :white_check_mark: |
|
||||
| Stock exchanges | 82 global exchanges | `intel_stock_exchanges` | :white_check_mark: |
|
||||
| Trade routes | 19 maritime chokepoints/routes | `intel_trade_routes` | :white_check_mark: |
|
||||
| Cloud regions | 28 AWS/Azure/GCP regions | `intel_cloud_regions` | :white_check_mark: |
|
||||
| Financial centers | 20 GFCI-ranked cities | `intel_financial_centers` | :white_check_mark: |
|
||||
|
||||
---
|
||||
|
||||
@@ -210,10 +215,12 @@ Live Starlette app with SSE streaming at `intel-dashboard --port 8501`.
|
||||
|
||||
| Feature | Status |
|
||||
|---------|--------|
|
||||
| SSE streaming (30s refresh) | :white_check_mark: 37 data streams |
|
||||
| Leaflet map (14 layers + 6 static) | :white_check_mark: |
|
||||
| 12 expandable drawer sections | :white_check_mark: |
|
||||
| SSE streaming (30s refresh) | :white_check_mark: 39 data streams |
|
||||
| Leaflet map (14 layers + 6 static + trade routes) | :white_check_mark: |
|
||||
| 14 expandable drawer sections | :white_check_mark: |
|
||||
| USNI Fleet Tracker drawer | :white_check_mark: |
|
||||
| BTC Technicals drawer (SMA, Mayer, cross signals) | :white_check_mark: |
|
||||
| Central Bank Rates drawer (15 banks) | :white_check_mark: |
|
||||
| Data freshness monitoring drawer | :white_check_mark: |
|
||||
| Per-source circuit breaker health | :white_check_mark: |
|
||||
| AI situation brief (Ollama-powered) | :white_check_mark: |
|
||||
@@ -266,16 +273,21 @@ Composite strategic posture assessment from 9 weighted domains (military, politi
|
||||
|
||||
Static datasets completed: 34 undersea cables, 48 AI datacenters, 27 spaceports, 27 critical mineral deposits, 82 stock exchanges. USNI Fleet Tracker for Navy disposition. RSS feeds expanded to 90+ across 16 categories (added Latin America 8+, multilingual ES/FR/DE 7+). Data freshness monitoring added to dashboard. Static HTML report generation removed (live dashboard replaces).
|
||||
|
||||
### Phase 12: Financial Intelligence & Geospatial Expansion (+5 = 84 tools)
|
||||
`intel_btc_technicals`, `intel_central_bank_rates`, `intel_trade_routes`, `intel_cloud_regions`, `intel_financial_centers`
|
||||
|
||||
BTC technical analysis with SMA-50/200, Mayer Multiple, golden/death cross signals, and ATH distance via CoinGecko historical data. Central bank policy rates for 15 major banks (Fed, ECB, BoE, BoJ, PBoC, RBI, RBA, BoC, SNB, BCB, BoK, CBRT, SARB, Banxico, BI) — live FRED data when API key set, curated fallback otherwise. Static geospatial datasets: 19 maritime trade routes/chokepoints with oil flow and vessel transit data, 28 cloud provider regions (AWS/Azure/GCP), 20 GFCI-ranked financial centers. Trade route markers added to dashboard infrastructure map layer. BTC technicals and central bank rates added to dashboard drawer.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Category | Have | Benchmark | Coverage |
|
||||
|----------|------|-----------|----------|
|
||||
| Data source tools | 80 | 42 | **190%** |
|
||||
| Data source tools | 84 | 42 | **200%** |
|
||||
| Analysis engines | 19 | 15 | **127%** |
|
||||
| Static datasets | 15 | 12 | **125%** |
|
||||
| Static datasets | 18 | 12 | **150%** |
|
||||
| RSS feeds | 90+ | 150+ | 60% |
|
||||
| Strategic synthesis | Posture + brief + fleet + exposure + USNI | Dashboard-only | **Exceeds** |
|
||||
|
||||
**Bottom line**: 80 tools across 30 domains, exceeding WorldMonitor benchmark by 90% in tool count, 27% in analysis engines, and 25% in static datasets. All phases 1-11 complete. Live Starlette dashboard with 37 SSE streams, 14 map layers, and data freshness monitoring. No remaining critical gaps — only niche sources (Wingbits requires API key, PizzInt has no public API).
|
||||
**Bottom line**: 84 tools across 30+ domains, exactly 2x WorldMonitor benchmark in tool count, 27% more analysis engines, and 50% more static datasets. All phases 1-12 complete. Live Starlette dashboard with 39 SSE streams, 14 map layers (with trade route markers), and data freshness monitoring.
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
"""Strategic maritime trade routes and chokepoints.
|
||||
|
||||
Pure data module — no I/O, no external dependencies.
|
||||
Sources: UNCTAD Review of Maritime Transport, US EIA World Transit Chokepoints,
|
||||
MarineTraffic, World Shipping Council.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TRADE_ROUTES — 19 critical maritime chokepoints and shipping lanes
|
||||
# Fields: name, lat, lon, type, daily_vessel_transits, oil_flow_mbd,
|
||||
# trade_value_pct, countries, notes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
TRADE_ROUTES: list[dict] = [
|
||||
{"name": "Strait of Hormuz", "lat": 26.57, "lon": 56.25, "type": "chokepoint",
|
||||
"daily_vessel_transits": 80, "oil_flow_mbd": 20.5, "trade_value_pct": 21,
|
||||
"countries": ["IRN", "OMN", "ARE"], "notes": "World's most critical oil chokepoint, ~20% of global oil"},
|
||||
{"name": "Strait of Malacca", "lat": 2.50, "lon": 101.50, "type": "chokepoint",
|
||||
"daily_vessel_transits": 200, "oil_flow_mbd": 16.0, "trade_value_pct": 25,
|
||||
"countries": ["MYS", "IDN", "SGP"], "notes": "Shortest route between Pacific and Indian Ocean, ~25% global trade"},
|
||||
{"name": "Suez Canal", "lat": 30.58, "lon": 32.27, "type": "canal",
|
||||
"daily_vessel_transits": 50, "oil_flow_mbd": 5.5, "trade_value_pct": 12,
|
||||
"countries": ["EGY"], "notes": "Connects Mediterranean and Red Sea, 12% global trade"},
|
||||
{"name": "Bab el-Mandeb", "lat": 12.58, "lon": 43.33, "type": "chokepoint",
|
||||
"daily_vessel_transits": 65, "oil_flow_mbd": 6.2, "trade_value_pct": 10,
|
||||
"countries": ["YEM", "DJI", "ERI"], "notes": "Gate to Suez Canal from Indian Ocean, Houthi threat zone"},
|
||||
{"name": "Panama Canal", "lat": 9.08, "lon": -79.68, "type": "canal",
|
||||
"daily_vessel_transits": 35, "oil_flow_mbd": 0.9, "trade_value_pct": 5,
|
||||
"countries": ["PAN"], "notes": "Connects Atlantic and Pacific, drought-restricted since 2023"},
|
||||
{"name": "Turkish Straits (Bosphorus)", "lat": 41.12, "lon": 29.05, "type": "chokepoint",
|
||||
"daily_vessel_transits": 120, "oil_flow_mbd": 3.0, "trade_value_pct": 3,
|
||||
"countries": ["TUR"], "notes": "Connects Black Sea to Mediterranean, Russian oil exports"},
|
||||
{"name": "Danish Straits", "lat": 55.60, "lon": 12.60, "type": "chokepoint",
|
||||
"daily_vessel_transits": 90, "oil_flow_mbd": 3.2, "trade_value_pct": 2,
|
||||
"countries": ["DNK", "SWE"], "notes": "Baltic Sea access, Russian energy exports, Nord Stream corridor"},
|
||||
{"name": "Strait of Gibraltar", "lat": 35.97, "lon": -5.60, "type": "chokepoint",
|
||||
"daily_vessel_transits": 250, "oil_flow_mbd": 3.0, "trade_value_pct": 4,
|
||||
"countries": ["ESP", "MAR", "GBR"], "notes": "Atlantic-Mediterranean gateway"},
|
||||
{"name": "Cape of Good Hope", "lat": -34.36, "lon": 18.47, "type": "route",
|
||||
"daily_vessel_transits": 40, "oil_flow_mbd": 6.0, "trade_value_pct": 8,
|
||||
"countries": ["ZAF"], "notes": "Suez Canal alternative, Houthi rerouting hub since 2024"},
|
||||
{"name": "Lombok Strait", "lat": -8.40, "lon": 115.70, "type": "chokepoint",
|
||||
"daily_vessel_transits": 30, "oil_flow_mbd": 1.5, "trade_value_pct": 2,
|
||||
"countries": ["IDN"], "notes": "Malacca bypass for deep-draft VLCCs"},
|
||||
{"name": "Taiwan Strait", "lat": 24.00, "lon": 118.50, "type": "chokepoint",
|
||||
"daily_vessel_transits": 150, "oil_flow_mbd": 5.0, "trade_value_pct": 20,
|
||||
"countries": ["TWN", "CHN"], "notes": "Semiconductor supply chain route, geopolitical flashpoint"},
|
||||
{"name": "Mozambique Channel", "lat": -17.00, "lon": 42.00, "type": "route",
|
||||
"daily_vessel_transits": 25, "oil_flow_mbd": 2.0, "trade_value_pct": 2,
|
||||
"countries": ["MOZ", "MDG"], "notes": "East African LNG export corridor"},
|
||||
{"name": "Strait of Sunda", "lat": -6.10, "lon": 105.80, "type": "chokepoint",
|
||||
"daily_vessel_transits": 15, "oil_flow_mbd": 0.5, "trade_value_pct": 1,
|
||||
"countries": ["IDN"], "notes": "Java-Sumatra strait, Malacca alternative"},
|
||||
{"name": "Northern Sea Route", "lat": 73.00, "lon": 100.00, "type": "route",
|
||||
"daily_vessel_transits": 5, "oil_flow_mbd": 0.3, "trade_value_pct": 0.1,
|
||||
"countries": ["RUS"], "notes": "Arctic route, seasonal, Russian-controlled, growing LNG traffic"},
|
||||
{"name": "English Channel", "lat": 50.50, "lon": 1.00, "type": "route",
|
||||
"daily_vessel_transits": 500, "oil_flow_mbd": 2.0, "trade_value_pct": 5,
|
||||
"countries": ["GBR", "FRA"], "notes": "Busiest shipping lane in the world by vessel count"},
|
||||
{"name": "Korea Strait", "lat": 34.00, "lon": 129.50, "type": "chokepoint",
|
||||
"daily_vessel_transits": 100, "oil_flow_mbd": 3.5, "trade_value_pct": 4,
|
||||
"countries": ["KOR", "JPN"], "notes": "Sea of Japan access, Japan-Korea trade corridor"},
|
||||
{"name": "Strait of Luzon", "lat": 20.00, "lon": 121.00, "type": "chokepoint",
|
||||
"daily_vessel_transits": 50, "oil_flow_mbd": 3.0, "trade_value_pct": 5,
|
||||
"countries": ["PHL", "TWN"], "notes": "South China Sea-Pacific gateway, PLAN patrol zone"},
|
||||
{"name": "Dover Strait", "lat": 51.05, "lon": 1.40, "type": "chokepoint",
|
||||
"daily_vessel_transits": 400, "oil_flow_mbd": 1.5, "trade_value_pct": 3,
|
||||
"countries": ["GBR", "FRA"], "notes": "Narrowest point of English Channel, TSS enforced"},
|
||||
{"name": "Dardanelles", "lat": 40.20, "lon": 26.40, "type": "chokepoint",
|
||||
"daily_vessel_transits": 120, "oil_flow_mbd": 3.0, "trade_value_pct": 3,
|
||||
"countries": ["TUR"], "notes": "Aegean entrance to Turkish Straits, grain corridor"},
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLOUD_REGIONS — Major cloud provider regions (AWS, Azure, GCP)
|
||||
# Fields: name, provider, region_code, lat, lon, launched, zone_count, notes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
CLOUD_REGIONS: list[dict] = [
|
||||
# AWS Major Regions
|
||||
{"name": "US East (Virginia)", "provider": "AWS", "region_code": "us-east-1", "lat": 39.05, "lon": -77.49, "launched": 2006, "zone_count": 6, "notes": "AWS's largest region, default for most services"},
|
||||
{"name": "US West (Oregon)", "provider": "AWS", "region_code": "us-west-2", "lat": 45.95, "lon": -119.57, "launched": 2011, "zone_count": 4, "notes": "Second-largest AWS region"},
|
||||
{"name": "Europe (Ireland)", "provider": "AWS", "region_code": "eu-west-1", "lat": 53.34, "lon": -6.26, "launched": 2007, "zone_count": 3, "notes": "Primary EU region"},
|
||||
{"name": "Europe (Frankfurt)", "provider": "AWS", "region_code": "eu-central-1", "lat": 50.11, "lon": 8.68, "launched": 2014, "zone_count": 3, "notes": "German data sovereignty"},
|
||||
{"name": "Asia Pacific (Tokyo)", "provider": "AWS", "region_code": "ap-northeast-1", "lat": 35.68, "lon": 139.77, "launched": 2011, "zone_count": 4, "notes": "AWS's largest APAC region"},
|
||||
{"name": "Asia Pacific (Singapore)", "provider": "AWS", "region_code": "ap-southeast-1", "lat": 1.35, "lon": 103.82, "launched": 2010, "zone_count": 3, "notes": "Southeast Asia hub"},
|
||||
{"name": "Asia Pacific (Sydney)", "provider": "AWS", "region_code": "ap-southeast-2", "lat": -33.87, "lon": 151.21, "launched": 2012, "zone_count": 3, "notes": "Oceania region"},
|
||||
{"name": "Middle East (Bahrain)", "provider": "AWS", "region_code": "me-south-1", "lat": 26.07, "lon": 50.55, "launched": 2019, "zone_count": 3, "notes": "First AWS MENA region"},
|
||||
{"name": "Africa (Cape Town)", "provider": "AWS", "region_code": "af-south-1", "lat": -33.93, "lon": 18.42, "launched": 2020, "zone_count": 3, "notes": "First AWS Africa region"},
|
||||
{"name": "South America (São Paulo)", "provider": "AWS", "region_code": "sa-east-1", "lat": -23.55, "lon": -46.63, "launched": 2011, "zone_count": 3, "notes": "Only AWS LatAm region"},
|
||||
# Azure Major Regions
|
||||
{"name": "East US", "provider": "Azure", "region_code": "eastus", "lat": 37.37, "lon": -79.15, "launched": 2010, "zone_count": 3, "notes": "Azure's largest region (Virginia)"},
|
||||
{"name": "West Europe", "provider": "Azure", "region_code": "westeurope", "lat": 52.37, "lon": 4.90, "launched": 2010, "zone_count": 3, "notes": "Netherlands, primary EU"},
|
||||
{"name": "North Europe", "provider": "Azure", "region_code": "northeurope", "lat": 53.35, "lon": -6.26, "launched": 2010, "zone_count": 3, "notes": "Ireland"},
|
||||
{"name": "Southeast Asia", "provider": "Azure", "region_code": "southeastasia", "lat": 1.28, "lon": 103.83, "launched": 2010, "zone_count": 3, "notes": "Singapore"},
|
||||
{"name": "Japan East", "provider": "Azure", "region_code": "japaneast", "lat": 35.68, "lon": 139.77, "launched": 2014, "zone_count": 3, "notes": "Tokyo, primary Japan"},
|
||||
{"name": "UAE North", "provider": "Azure", "region_code": "uaenorth", "lat": 25.27, "lon": 55.30, "launched": 2019, "zone_count": 3, "notes": "Dubai"},
|
||||
{"name": "South Africa North", "provider": "Azure", "region_code": "southafricanorth", "lat": -25.73, "lon": 28.22, "launched": 2019, "zone_count": 3, "notes": "Johannesburg"},
|
||||
{"name": "Brazil South", "provider": "Azure", "region_code": "brazilsouth", "lat": -23.55, "lon": -46.63, "launched": 2014, "zone_count": 3, "notes": "São Paulo"},
|
||||
# GCP Major Regions
|
||||
{"name": "US Central (Iowa)", "provider": "GCP", "region_code": "us-central1", "lat": 41.26, "lon": -95.86, "launched": 2012, "zone_count": 4, "notes": "GCP's largest region, Council Bluffs"},
|
||||
{"name": "US East (South Carolina)", "provider": "GCP", "region_code": "us-east1", "lat": 33.84, "lon": -81.16, "launched": 2015, "zone_count": 3, "notes": "Moncks Corner"},
|
||||
{"name": "Europe West (Belgium)", "provider": "GCP", "region_code": "europe-west1", "lat": 50.45, "lon": 3.82, "launched": 2015, "zone_count": 3, "notes": "St. Ghislain"},
|
||||
{"name": "Europe West (London)", "provider": "GCP", "region_code": "europe-west2", "lat": 51.51, "lon": -0.13, "launched": 2017, "zone_count": 3, "notes": "London"},
|
||||
{"name": "Asia East (Taiwan)", "provider": "GCP", "region_code": "asia-east1", "lat": 24.05, "lon": 120.69, "launched": 2014, "zone_count": 3, "notes": "Changhua County"},
|
||||
{"name": "Asia Northeast (Tokyo)", "provider": "GCP", "region_code": "asia-northeast1", "lat": 35.68, "lon": 139.77, "launched": 2016, "zone_count": 3, "notes": "Tokyo"},
|
||||
{"name": "Asia Southeast (Singapore)", "provider": "GCP", "region_code": "asia-southeast1", "lat": 1.35, "lon": 103.82, "launched": 2017, "zone_count": 3, "notes": "Jurong West"},
|
||||
{"name": "Australia Southeast (Melbourne)", "provider": "GCP", "region_code": "australia-southeast1", "lat": -37.81, "lon": 144.96, "launched": 2017, "zone_count": 3, "notes": "Melbourne"},
|
||||
{"name": "South America East (São Paulo)", "provider": "GCP", "region_code": "southamerica-east1", "lat": -23.55, "lon": -46.63, "launched": 2017, "zone_count": 3, "notes": "Osasco"},
|
||||
{"name": "Middle East (Tel Aviv)", "provider": "GCP", "region_code": "me-west1", "lat": 32.09, "lon": 34.77, "launched": 2022, "zone_count": 3, "notes": "Israel"},
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# FINANCIAL_CENTERS — GFCI top 20 + key regional centers
|
||||
# Fields: name, country, iso3, lat, lon, gfci_rank, gfci_rating,
|
||||
# specialization, exchange, notes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
FINANCIAL_CENTERS: list[dict] = [
|
||||
{"name": "New York", "country": "USA", "iso3": "USA", "lat": 40.71, "lon": -74.01, "gfci_rank": 1, "gfci_rating": 760, "specialization": "equities, derivatives, banking", "exchange": "NYSE/NASDAQ", "notes": "World's largest financial center, $25T+ equity market cap"},
|
||||
{"name": "London", "country": "UK", "iso3": "GBR", "lat": 51.51, "lon": -0.13, "gfci_rank": 2, "gfci_rating": 744, "specialization": "forex, insurance, banking", "exchange": "LSE", "notes": "Largest FX trading hub (~38% global volume)"},
|
||||
{"name": "Singapore", "country": "Singapore", "iso3": "SGP", "lat": 1.28, "lon": 103.85, "gfci_rank": 3, "gfci_rating": 742, "specialization": "wealth management, FX, commodities", "exchange": "SGX", "notes": "Asia-Pacific wealth management hub"},
|
||||
{"name": "Hong Kong", "country": "China (SAR)", "iso3": "HKG", "lat": 22.32, "lon": 114.17, "gfci_rank": 4, "gfci_rating": 741, "specialization": "IPOs, banking, yuan offshore", "exchange": "HKEX", "notes": "Gateway to mainland China capital markets"},
|
||||
{"name": "San Francisco", "country": "USA", "iso3": "USA", "lat": 37.77, "lon": -122.42, "gfci_rank": 5, "gfci_rating": 738, "specialization": "venture capital, fintech", "exchange": "—", "notes": "Global VC capital, tech finance hub"},
|
||||
{"name": "Shanghai", "country": "China", "iso3": "CHN", "lat": 31.23, "lon": 121.47, "gfci_rank": 6, "gfci_rating": 735, "specialization": "equities, bonds, commodities", "exchange": "SSE", "notes": "Largest exchange in mainland China"},
|
||||
{"name": "Los Angeles", "country": "USA", "iso3": "USA", "lat": 34.05, "lon": -118.24, "gfci_rank": 7, "gfci_rating": 733, "specialization": "entertainment finance, VC", "exchange": "—", "notes": "Growing fintech ecosystem"},
|
||||
{"name": "Tokyo", "country": "Japan", "iso3": "JPN", "lat": 35.68, "lon": 139.77, "gfci_rank": 8, "gfci_rating": 730, "specialization": "equities, bonds, banking", "exchange": "JPX", "notes": "Third-largest equity market globally"},
|
||||
{"name": "Seoul", "country": "South Korea", "iso3": "KOR", "lat": 37.57, "lon": 126.98, "gfci_rank": 9, "gfci_rating": 728, "specialization": "equities, crypto, semiconductors", "exchange": "KRX", "notes": "World's busiest crypto trading market"},
|
||||
{"name": "Shenzhen", "country": "China", "iso3": "CHN", "lat": 22.54, "lon": 114.06, "gfci_rank": 10, "gfci_rating": 726, "specialization": "tech equities, IPOs", "exchange": "SZSE", "notes": "ChiNext board, tech-heavy market"},
|
||||
{"name": "Chicago", "country": "USA", "iso3": "USA", "lat": 41.88, "lon": -87.63, "gfci_rank": 11, "gfci_rating": 724, "specialization": "derivatives, commodities, futures", "exchange": "CME/CBOT", "notes": "World's largest derivatives exchange"},
|
||||
{"name": "Sydney", "country": "Australia", "iso3": "AUS", "lat": -33.87, "lon": 151.21, "gfci_rank": 12, "gfci_rating": 722, "specialization": "mining, superannuation", "exchange": "ASX", "notes": "Largest exchange in Southern Hemisphere"},
|
||||
{"name": "Zurich", "country": "Switzerland", "iso3": "CHE", "lat": 47.37, "lon": 8.54, "gfci_rank": 13, "gfci_rating": 720, "specialization": "private banking, insurance", "exchange": "SIX", "notes": "Global private banking capital"},
|
||||
{"name": "Frankfurt", "country": "Germany", "iso3": "DEU", "lat": 50.11, "lon": 8.68, "gfci_rank": 14, "gfci_rating": 718, "specialization": "banking, ECB HQ", "exchange": "Xetra/FWB", "notes": "ECB headquarters, eurozone financial hub"},
|
||||
{"name": "Dubai", "country": "UAE", "iso3": "ARE", "lat": 25.20, "lon": 55.27, "gfci_rank": 15, "gfci_rating": 716, "specialization": "Islamic finance, wealth management", "exchange": "DFM/DIFC", "notes": "Bridge between East and West, DIFC free zone"},
|
||||
{"name": "Paris", "country": "France", "iso3": "FRA", "lat": 48.86, "lon": 2.35, "gfci_rank": 16, "gfci_rating": 714, "specialization": "insurance, asset management", "exchange": "Euronext Paris", "notes": "Post-Brexit gains in euro clearing"},
|
||||
{"name": "Mumbai", "country": "India", "iso3": "IND", "lat": 19.08, "lon": 72.88, "gfci_rank": 17, "gfci_rating": 712, "specialization": "equities, derivatives, banking", "exchange": "BSE/NSE", "notes": "NSE is world's largest derivatives exchange by volume"},
|
||||
{"name": "Toronto", "country": "Canada", "iso3": "CAN", "lat": 43.65, "lon": -79.38, "gfci_rank": 18, "gfci_rating": 710, "specialization": "mining, banking, cannabis", "exchange": "TSX", "notes": "World's #1 mining finance exchange"},
|
||||
{"name": "Abu Dhabi", "country": "UAE", "iso3": "ARE", "lat": 24.45, "lon": 54.65, "gfci_rank": 19, "gfci_rating": 708, "specialization": "sovereign wealth, energy finance", "exchange": "ADX/ADGM", "notes": "ADIA (world's 3rd largest SWF), energy focus"},
|
||||
{"name": "Geneva", "country": "Switzerland", "iso3": "CHE", "lat": 46.20, "lon": 6.14, "gfci_rank": 20, "gfci_rating": 706, "specialization": "private banking, commodity trading", "exchange": "—", "notes": "Global commodity trading capital, wealth management"},
|
||||
]
|
||||
@@ -56,6 +56,8 @@ from world_intel_mcp.sources.usni_fleet import fetch_usni_fleet
|
||||
from world_intel_mcp.config.countries import INTEL_HOTSPOTS, STRATEGIC_WATERWAYS
|
||||
from world_intel_mcp.config.geospatial import MILITARY_BASES, STRATEGIC_PORTS, PIPELINES, NUCLEAR_FACILITIES
|
||||
from world_intel_mcp.sources.infrastructure import CABLE_CORRIDORS
|
||||
from world_intel_mcp.config.trade_routes import TRADE_ROUTES, CLOUD_REGIONS, FINANCIAL_CENTERS
|
||||
from world_intel_mcp.sources.central_banks import fetch_central_bank_rates
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -127,6 +129,8 @@ async def _fetch_overview() -> dict:
|
||||
"traffic_flow": traffic.fetch_traffic_flow(fetcher),
|
||||
"traffic_incidents": traffic.fetch_traffic_incidents(fetcher),
|
||||
"webcams": webcams.fetch_webcams(fetcher),
|
||||
"btc_technicals": markets.fetch_btc_technicals(fetcher),
|
||||
"central_bank_rates": fetch_central_bank_rates(fetcher),
|
||||
}
|
||||
|
||||
# Per-coro timeout so no single slow source blocks the entire dashboard.
|
||||
@@ -187,6 +191,9 @@ async def _fetch_overview() -> dict:
|
||||
result["pipelines"] = {"pipelines": PIPELINES, "count": len(PIPELINES)}
|
||||
result["nuclear_facilities"] = {"facilities": NUCLEAR_FACILITIES, "count": len(NUCLEAR_FACILITIES)}
|
||||
result["waterways"] = {"waterways": STRATEGIC_WATERWAYS, "count": len(STRATEGIC_WATERWAYS)}
|
||||
result["trade_routes"] = {"routes": TRADE_ROUTES, "count": len(TRADE_ROUTES)}
|
||||
result["cloud_regions"] = {"regions": CLOUD_REGIONS, "count": len(CLOUD_REGIONS)}
|
||||
result["financial_centers"] = {"centers": FINANCIAL_CENTERS, "count": len(FINANCIAL_CENTERS)}
|
||||
result["cable_corridors"] = {
|
||||
"corridors": [
|
||||
{"name": n, "lat_range": c["lat_range"], "lon_range": c["lon_range"], "cables": c["cables"]}
|
||||
@@ -275,6 +282,9 @@ async def api_static(request):
|
||||
],
|
||||
"count": len(CABLE_CORRIDORS),
|
||||
},
|
||||
"trade_routes": {"routes": TRADE_ROUTES, "count": len(TRADE_ROUTES)},
|
||||
"cloud_regions": {"regions": CLOUD_REGIONS, "count": len(CLOUD_REGIONS)},
|
||||
"financial_centers": {"centers": FINANCIAL_CENTERS, "count": len(FINANCIAL_CENTERS)},
|
||||
}, headers={"Access-Control-Allow-Origin": "*"})
|
||||
|
||||
|
||||
|
||||
@@ -140,6 +140,14 @@ a { color: var(--accent); text-decoration: none; }
|
||||
padding: 5px 0; cursor: pointer; transition: opacity 0.2s;
|
||||
}
|
||||
.layer-row:hover { opacity: 0.85; }
|
||||
.rgn-btn {
|
||||
font-family: var(--mono); font-size: 0.6rem; letter-spacing: 0.5px;
|
||||
padding: 3px 7px; border: 1px solid rgba(255,255,255,0.12); border-radius: 4px;
|
||||
background: rgba(255,255,255,0.04); color: var(--dim); cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.rgn-btn:hover { background: rgba(255,255,255,0.1); color: var(--text); }
|
||||
.rgn-btn.active { background: rgba(0,229,255,0.15); color: var(--accent); border-color: rgba(0,229,255,0.3); }
|
||||
.layer-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||
.layer-label { font-size: 0.75rem; color: var(--text); flex: 1; }
|
||||
.layer-count {
|
||||
@@ -403,6 +411,21 @@ a { color: var(--accent); text-decoration: none; }
|
||||
cursor: pointer; transition: transform 0.15s, opacity 0.15s;
|
||||
}
|
||||
.mk-waterway:hover { transform: rotate(45deg) scale(1.5); opacity: 1; }
|
||||
.mk-chokepoint {
|
||||
width: 8px; height: 8px; background: #ff5722; border-radius: 50%; opacity: 0.9;
|
||||
border: 1.5px solid rgba(255,87,34,0.6); cursor: pointer; transition: transform 0.15s;
|
||||
}
|
||||
.mk-chokepoint:hover { transform: scale(1.8); }
|
||||
.mk-canal {
|
||||
width: 8px; height: 8px; background: #ffab00; border-radius: 2px; opacity: 0.9;
|
||||
border: 1.5px solid rgba(255,171,0,0.6); cursor: pointer; transition: transform 0.15s;
|
||||
}
|
||||
.mk-canal:hover { transform: scale(1.8); }
|
||||
.mk-sealane {
|
||||
width: 6px; height: 6px; background: #00bfa5; border-radius: 50%; opacity: 0.7;
|
||||
border: 1px solid rgba(0,191,165,0.5); cursor: pointer; transition: transform 0.15s;
|
||||
}
|
||||
.mk-sealane:hover { transform: scale(1.8); }
|
||||
.cable-corridor { cursor: pointer; transition: fill-opacity 0.15s; }
|
||||
.cable-corridor:hover { fill-opacity: 0.1 !important; stroke-opacity: 0.5 !important; }
|
||||
.mk-plane {
|
||||
@@ -654,6 +677,27 @@ a { color: var(--accent); text-decoration: none; }
|
||||
<span class="layer-count" id="cntNews">0</span>
|
||||
<div class="toggle on" style="--lc:var(--accent)"><div class="knob"></div></div>
|
||||
</div>
|
||||
<div style="border-top:1px solid rgba(255,255,255,0.08);margin-top:4px;padding-top:6px">
|
||||
<div style="font-size:0.55rem;letter-spacing:1px;color:rgba(255,255,255,0.35);margin-bottom:4px">REGION</div>
|
||||
<div id="regionPresets" style="display:flex;flex-wrap:wrap;gap:2px">
|
||||
<button class="rgn-btn active" data-region="global">Globe</button>
|
||||
<button class="rgn-btn" data-region="americas">Americas</button>
|
||||
<button class="rgn-btn" data-region="europe">Europe</button>
|
||||
<button class="rgn-btn" data-region="mena">MENA</button>
|
||||
<button class="rgn-btn" data-region="asia">Asia-Pac</button>
|
||||
<button class="rgn-btn" data-region="africa">Africa</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="border-top:1px solid rgba(255,255,255,0.08);margin-top:4px;padding-top:6px">
|
||||
<div style="font-size:0.55rem;letter-spacing:1px;color:rgba(255,255,255,0.35);margin-bottom:4px">TIME WINDOW</div>
|
||||
<div id="timeFilter" style="display:flex;gap:2px">
|
||||
<button class="rgn-btn" data-hours="1">1h</button>
|
||||
<button class="rgn-btn" data-hours="6">6h</button>
|
||||
<button class="rgn-btn active" data-hours="24">24h</button>
|
||||
<button class="rgn-btn" data-hours="168">7d</button>
|
||||
<button class="rgn-btn" data-hours="0">All</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- DATA DRAWER -->
|
||||
@@ -1271,7 +1315,9 @@ document.addEventListener('keydown', function(e) {
|
||||
function updateMapQuakes(data) {
|
||||
mapLayers.quakes.clearLayers();
|
||||
if (!data || data.error) { $('#cntQuakes').textContent = '0'; return; }
|
||||
var quakes = data.earthquakes || [];
|
||||
var quakes = (data.earthquakes || []).filter(function(q) {
|
||||
return _withinTimeWindow(q.time || q.timestamp);
|
||||
});
|
||||
$('#cntQuakes').textContent = quakes.length;
|
||||
quakes.slice(0, 100).forEach(function(q) {
|
||||
if (q.latitude == null || q.longitude == null) return;
|
||||
@@ -1490,6 +1536,27 @@ function updateMapInfra(data) {
|
||||
mk.bindTooltip('<b>' + esc(w.name) + '</b><br><span style="opacity:0.7">' + esc(w.throughput || '') + '</span>', { className: 'mk-tip', direction: 'top', offset: [0, -6] });
|
||||
mk.addTo(mapLayers.infra);
|
||||
});
|
||||
// Trade routes / maritime chokepoints (anchor markers)
|
||||
var trs = (data.trade_routes && data.trade_routes.routes) || [];
|
||||
trs.forEach(function(tr) {
|
||||
if (tr.lat == null || tr.lon == null) return;
|
||||
total++;
|
||||
var iconCls = tr.type === 'chokepoint' ? 'mk-chokepoint' : tr.type === 'canal' ? 'mk-canal' : 'mk-sealane';
|
||||
var mk = L.marker([tr.lat, tr.lon], {
|
||||
icon: L.divIcon({ className: iconCls, iconSize: [8, 8], iconAnchor: [4, 4] })
|
||||
});
|
||||
mk.bindTooltip('<b>' + esc(tr.name) + '</b><br>' +
|
||||
'<span style="opacity:0.7">' + esc(tr.type) + ' · ' + (tr.oil_flow_mbd || 0) + ' mbd oil · ~' + (tr.daily_vessel_transits || 0) + ' vessels/day</span>',
|
||||
{ className: 'mk-tip', direction: 'top', offset: [0, -5] });
|
||||
mk.on('click', function() {
|
||||
showDetail('trade_route', {
|
||||
name: tr.name, type: tr.type, oil_flow_mbd: tr.oil_flow_mbd,
|
||||
daily_transits: tr.daily_vessel_transits, trade_value_pct: tr.trade_value_pct,
|
||||
countries: (tr.countries || []).join(', '), notes: tr.notes
|
||||
});
|
||||
});
|
||||
mk.addTo(mapLayers.infra);
|
||||
});
|
||||
// Submarine cable corridors (shaded rectangles)
|
||||
var cables = (data.cable_corridors && data.cable_corridors.corridors) || [];
|
||||
var cableStyle = { color: '#4da8ff', weight: 1, opacity: 0.2, fillColor: '#4da8ff', fillOpacity: 0.04, dashArray: '4 3', className: 'cable-corridor', interactive: false };
|
||||
@@ -1677,7 +1744,9 @@ function _geolocateArticle(article) {
|
||||
function updateMapNews(data) {
|
||||
mapLayers.news.clearLayers();
|
||||
if (!data || data.error) { $('#cntNews').textContent = '0'; return; }
|
||||
var articles = data.articles || data.items || [];
|
||||
var articles = (data.articles || data.items || []).filter(function(a) {
|
||||
return _withinTimeWindow(a.published || a.pub_date || a.timestamp);
|
||||
});
|
||||
var placed = 0;
|
||||
articles.forEach(function(a) {
|
||||
var geo = _geolocateArticle(a);
|
||||
@@ -2488,6 +2557,46 @@ function updateDrawer(data) {
|
||||
});
|
||||
}
|
||||
|
||||
// ── BTC TECHNICALS ──
|
||||
if (data.btc_technicals && !data.btc_technicals.error && data.btc_technicals.price) {
|
||||
var bt = data.btc_technicals;
|
||||
h += '<div class="sh">BTC TECHNICALS</div>';
|
||||
var crossCls = bt.cross_signal === 'golden_cross' ? 'good' : bt.cross_signal === 'death_cross' ? 'crit' : 'dim';
|
||||
var crossLabel = bt.cross_signal === 'golden_cross' ? 'GOLDEN CROSS' : bt.cross_signal === 'death_cross' ? 'DEATH CROSS' : 'NEUTRAL';
|
||||
h += '<div class="mini-boxes" style="margin:6px 0">';
|
||||
h += '<div class="mini-box"><div class="v">$' + num(bt.price) + '</div><div class="l">BTC Price</div></div>';
|
||||
h += '<div class="mini-box"><div class="v">' + (bt.mayer_multiple || '—') + '</div><div class="l">Mayer Multiple</div></div>';
|
||||
h += '<div class="mini-box"><div class="v ' + crossCls + '">' + crossLabel + '</div><div class="l">Signal</div></div>';
|
||||
h += '</div>';
|
||||
h += '<div style="font-size:0.65rem;padding:2px 0"><span class="bright">SMA-50</span> <span class="dim">$' + num(bt.sma_50) + '</span></div>';
|
||||
if (bt.sma_200) h += '<div style="font-size:0.65rem;padding:1px 0"><span class="bright">SMA-200</span> <span class="dim">$' + num(bt.sma_200) + '</span></div>';
|
||||
if (bt.change_7d_pct != null) {
|
||||
var c7 = bt.change_7d_pct >= 0 ? 'good' : 'crit';
|
||||
h += '<div style="font-size:0.65rem;padding:1px 0"><span class="bright">7d</span> <span class="' + c7 + '">' + (bt.change_7d_pct > 0 ? '+' : '') + bt.change_7d_pct + '%</span>';
|
||||
if (bt.change_30d_pct != null) {
|
||||
var c30 = bt.change_30d_pct >= 0 ? 'good' : 'crit';
|
||||
h += ' <span class="bright">30d</span> <span class="' + c30 + '">' + (bt.change_30d_pct > 0 ? '+' : '') + bt.change_30d_pct + '%</span>';
|
||||
}
|
||||
h += '</div>';
|
||||
}
|
||||
if (bt.ath_distance_pct != null) h += '<div style="font-size:0.65rem;padding:1px 0"><span class="bright">From ATH</span> <span class="dim">' + bt.ath_distance_pct + '%</span></div>';
|
||||
}
|
||||
|
||||
// ── CENTRAL BANK RATES ──
|
||||
if (data.central_bank_rates && !data.central_bank_rates.error && (data.central_bank_rates.rates || []).length > 0) {
|
||||
var cbr = data.central_bank_rates;
|
||||
h += '<div class="sh">CENTRAL BANK RATES</div>';
|
||||
h += '<div class="dim" style="font-size:0.6rem;padding:2px 0">' + cbr.count + ' banks' + (cbr.fred_available ? ' (FRED live)' : ' (curated)') + '</div>';
|
||||
cbr.rates.forEach(function(r) {
|
||||
var rateCls = r.rate >= 10 ? 'crit' : r.rate >= 5 ? 'warn' : r.rate >= 2 ? 'bright' : 'good';
|
||||
h += '<div style="font-size:0.65rem;padding:1px 0">';
|
||||
h += '<span class="' + rateCls + '" style="min-width:40px;display:inline-block;text-align:right">' + r.rate.toFixed(2) + '%</span> ';
|
||||
h += '<span class="bright">' + esc(r.label) + '</span>';
|
||||
if (r.country) h += ' <span class="dim">(' + esc(r.country) + ')</span>';
|
||||
h += '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
// ── DATA FRESHNESS ──
|
||||
if (data.cache_freshness && Object.keys(data.cache_freshness).length > 0) {
|
||||
var cf = data.cache_freshness;
|
||||
@@ -2550,8 +2659,11 @@ function updateTicker(data) {
|
||||
|
||||
// ════════════ MASTER UPDATE ════════════
|
||||
|
||||
function refreshDashboard(data) { updateAll(data); }
|
||||
|
||||
function updateAll(data) {
|
||||
latestData = data;
|
||||
window._lastSSEData = data;
|
||||
|
||||
// Map layers (each wrapped to prevent cascade failures)
|
||||
var conflictData = (data.acled_events && !data.acled_events.error && data.acled_events.count > 0) ? data.acled_events
|
||||
@@ -2781,6 +2893,72 @@ window.addEventListener('resize', _syncLayersTop);
|
||||
$('#hudStats').innerHTML = '<div class="stat-pill"><span class="l" style="animation:pulse-dot 2s ease-in-out infinite">LOADING LIVE FEEDS</span></div>';
|
||||
$('#drawerBody').innerHTML = '<div style="padding:30px;text-align:center"><div class="load-ring" style="width:28px;height:28px;margin:0 auto 10px"></div><div class="dim" style="font-size:0.6rem;letter-spacing:1.5px">LOADING INTELLIGENCE FEEDS</div></div>';
|
||||
|
||||
// ═══════════ REGIONAL PRESETS ═══════════
|
||||
var REGION_VIEWS = {
|
||||
global: { center: [20, 0], zoom: 3 },
|
||||
americas: { center: [10, -80], zoom: 4 },
|
||||
europe: { center: [50, 15], zoom: 5 },
|
||||
mena: { center: [28, 40], zoom: 5 },
|
||||
asia: { center: [20, 110], zoom: 4 },
|
||||
africa: { center: [0, 25], zoom: 4 },
|
||||
};
|
||||
$$('#regionPresets .rgn-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var region = btn.dataset.region;
|
||||
var view = REGION_VIEWS[region];
|
||||
if (!view) return;
|
||||
$$('#regionPresets .rgn-btn').forEach(function(b) { b.classList.remove('active'); });
|
||||
btn.classList.add('active');
|
||||
map.flyTo(view.center, view.zoom, { duration: 1.2 });
|
||||
try { localStorage.setItem('phoenix-region', region); } catch(e) {}
|
||||
});
|
||||
});
|
||||
// Restore saved region
|
||||
try {
|
||||
var savedRegion = localStorage.getItem('phoenix-region');
|
||||
if (savedRegion && REGION_VIEWS[savedRegion]) {
|
||||
$$('#regionPresets .rgn-btn').forEach(function(b) {
|
||||
b.classList.toggle('active', b.dataset.region === savedRegion);
|
||||
});
|
||||
var sv = REGION_VIEWS[savedRegion];
|
||||
map.setView(sv.center, sv.zoom);
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
// ═══════════ TIME WINDOW FILTER ═══════════
|
||||
var _timeFilterHours = 24;
|
||||
$$('#timeFilter .rgn-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
_timeFilterHours = parseInt(btn.dataset.hours) || 0;
|
||||
$$('#timeFilter .rgn-btn').forEach(function(b) { b.classList.remove('active'); });
|
||||
btn.classList.add('active');
|
||||
try { localStorage.setItem('phoenix-time-filter', String(_timeFilterHours)); } catch(e) {}
|
||||
// Re-render with current data if available
|
||||
if (window._lastSSEData) refreshDashboard(window._lastSSEData);
|
||||
});
|
||||
});
|
||||
// Restore saved time filter
|
||||
try {
|
||||
var savedTime = localStorage.getItem('phoenix-time-filter');
|
||||
if (savedTime !== null) {
|
||||
_timeFilterHours = parseInt(savedTime) || 0;
|
||||
$$('#timeFilter .rgn-btn').forEach(function(b) {
|
||||
b.classList.toggle('active', b.dataset.hours === savedTime);
|
||||
});
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
// Time filter helper: check if an ISO timestamp is within the active window
|
||||
function _withinTimeWindow(isoTimestamp) {
|
||||
if (_timeFilterHours === 0) return true; // "All" = no filter
|
||||
if (!isoTimestamp) return true; // No timestamp = show
|
||||
try {
|
||||
var ts = new Date(isoTimestamp).getTime();
|
||||
var cutoff = Date.now() - (_timeFilterHours * 3600000);
|
||||
return ts >= cutoff;
|
||||
} catch(e) { return true; }
|
||||
}
|
||||
|
||||
// Load static geospatial data immediately (bases, ports, nuclear facilities).
|
||||
fetch('/api/static')
|
||||
.then(function(r) { return r.json(); })
|
||||
|
||||
@@ -21,6 +21,8 @@ Phase 11: Strategic synthesis — strategic posture, world brief, fleet report,
|
||||
Phase 12: Extended geospatial (cables, datacenters, spaceports, minerals, exchanges), country stocks,
|
||||
aircraft batch, Hacker News, GitHub trending, arXiv papers, USA spending,
|
||||
NASA EONET, GDACS disaster alerts (+14 = 82 tools).
|
||||
Phase 13: USNI fleet tracker, RSS expansion, report removal.
|
||||
Phase 14: BTC technicals, central bank rates, trade routes, cloud regions, financial centers (+5 = 87 tools).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -36,7 +38,7 @@ from mcp.types import Tool, TextContent
|
||||
from .cache import Cache
|
||||
from .circuit_breaker import CircuitBreaker
|
||||
from .fetcher import Fetcher
|
||||
from .sources import markets, economic, seismology, wildfire, conflict, military, infrastructure, maritime, climate, news, intelligence, prediction, displacement, aviation, cyber, space_weather, ai_watch, health, sanctions, elections, shipping, social, nuclear, service_status, geospatial, hacker_news, github_trending, arxiv_papers, usa_spending, environmental, usni_fleet
|
||||
from .sources import markets, economic, seismology, wildfire, conflict, military, infrastructure, maritime, climate, news, intelligence, prediction, displacement, aviation, cyber, space_weather, ai_watch, health, sanctions, elections, shipping, social, nuclear, service_status, geospatial, hacker_news, github_trending, arxiv_papers, usa_spending, environmental, usni_fleet, central_banks
|
||||
|
||||
logging.basicConfig(
|
||||
level=os.environ.get("WORLD_INTEL_LOG_LEVEL", "INFO"),
|
||||
@@ -851,6 +853,54 @@ TOOLS: list[Tool] = [
|
||||
},
|
||||
},
|
||||
),
|
||||
# --- BTC Technicals (1 tool) ---
|
||||
Tool(
|
||||
name="intel_btc_technicals",
|
||||
description="Bitcoin technical indicators: SMA-50, SMA-200, Mayer Multiple, golden/death cross, distance from ATH, 7d/30d changes.",
|
||||
inputSchema={"type": "object", "properties": {}},
|
||||
),
|
||||
# --- Central Banks (1 tool) ---
|
||||
Tool(
|
||||
name="intel_central_bank_rates",
|
||||
description="Policy rates for 15 major central banks: Fed, ECB, BoE, BoJ, PBoC, RBI, RBA, BoC, SNB, BCB, BoK, CBRT, SARB, Banxico, BI. Live FRED data when API key set, curated fallback otherwise.",
|
||||
inputSchema={"type": "object", "properties": {}},
|
||||
),
|
||||
# --- Trade Routes (1 tool) ---
|
||||
Tool(
|
||||
name="intel_trade_routes",
|
||||
description="19 critical maritime chokepoints and trade routes with oil flow (mbd), daily vessel transits, trade value share. Optional: route_type (chokepoint/canal/route), country (ISO-3).",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"route_type": {"type": "string", "description": "Filter: chokepoint, canal, route"},
|
||||
"country": {"type": "string", "description": "Filter by ISO-3 country code"},
|
||||
},
|
||||
},
|
||||
),
|
||||
# --- Cloud Regions (1 tool) ---
|
||||
Tool(
|
||||
name="intel_cloud_regions",
|
||||
description="28 major cloud provider regions (AWS, Azure, GCP) with coordinates, zone counts, and launch dates. Optional: provider, country.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {"type": "string", "description": "Filter: AWS, Azure, GCP"},
|
||||
"country": {"type": "string", "description": "Filter by region name substring"},
|
||||
},
|
||||
},
|
||||
),
|
||||
# --- Financial Centers (1 tool) ---
|
||||
Tool(
|
||||
name="intel_financial_centers",
|
||||
description="GFCI top 20 global financial centers with rankings, ratings, specializations, and exchange info. Optional: country (ISO-3), min_rank.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"country": {"type": "string", "description": "Filter by ISO-3 country code"},
|
||||
"min_rank": {"type": "integer", "description": "Only include centers ranked this or better"},
|
||||
},
|
||||
},
|
||||
),
|
||||
# --- System (1 tool) ---
|
||||
Tool(
|
||||
name="intel_status",
|
||||
@@ -1205,6 +1255,35 @@ async def _dispatch(name: str, arguments: dict[str, Any]) -> Any:
|
||||
case "intel_usni_fleet":
|
||||
return await usni_fleet.fetch_usni_fleet(fetcher)
|
||||
|
||||
# BTC Technicals
|
||||
case "intel_btc_technicals":
|
||||
return await markets.fetch_btc_technicals(fetcher)
|
||||
|
||||
# Central Bank Rates
|
||||
case "intel_central_bank_rates":
|
||||
return await central_banks.fetch_central_bank_rates(fetcher)
|
||||
|
||||
# Trade Routes
|
||||
case "intel_trade_routes":
|
||||
return await geospatial.fetch_trade_routes(
|
||||
route_type=arguments.get("route_type"),
|
||||
country=arguments.get("country"),
|
||||
)
|
||||
|
||||
# Cloud Regions
|
||||
case "intel_cloud_regions":
|
||||
return await geospatial.fetch_cloud_regions(
|
||||
provider=arguments.get("provider"),
|
||||
country=arguments.get("country"),
|
||||
)
|
||||
|
||||
# Financial Centers
|
||||
case "intel_financial_centers":
|
||||
return await geospatial.fetch_financial_centers(
|
||||
country=arguments.get("country"),
|
||||
min_rank=arguments.get("min_rank"),
|
||||
)
|
||||
|
||||
# Environmental
|
||||
case "intel_environmental_events":
|
||||
return await environmental.fetch_environmental_events(
|
||||
@@ -1274,7 +1353,7 @@ async def _dispatch(name: str, arguments: dict[str, Any]) -> Any:
|
||||
"social": ["reddit-public"],
|
||||
"nuclear": ["usgs-nuclear-monitor"],
|
||||
"service_status": ["aws", "azure", "gcp", "cloudflare", "github"],
|
||||
"geospatial": ["static-datasets (bases, ports, pipelines, nuclear, cables, datacenters, spaceports, minerals, exchanges)"],
|
||||
"geospatial": ["static-datasets (bases, ports, pipelines, nuclear, cables, datacenters, spaceports, minerals, exchanges, trade-routes, cloud-regions, financial-centers)"],
|
||||
"nlp": ["regex-ner", "keyword-classifier", "jaccard-clustering", "keyword-spike-detector"],
|
||||
"synthesis": ["strategic-posture", "world-brief", "fleet-report", "population-exposure"],
|
||||
"tech": ["hackernews", "github", "arxiv"],
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
"""Central bank policy rates from FRED and curated data.
|
||||
|
||||
Fetches the Federal Funds Rate, ECB deposit rate, and other major central
|
||||
bank policy rates. Uses FRED API when available (free key), falls back to
|
||||
a curated static dataset for non-FRED banks.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from ..fetcher import Fetcher
|
||||
|
||||
logger = logging.getLogger("world-intel-mcp.sources.central_banks")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# FRED series IDs for central bank rates
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_FRED_RATE_SERIES: dict[str, dict] = {
|
||||
"fed_funds": {
|
||||
"series_id": "DFF",
|
||||
"bank": "Federal Reserve",
|
||||
"country": "USA",
|
||||
"currency": "USD",
|
||||
"label": "Federal Funds Rate",
|
||||
},
|
||||
"ecb_deposit": {
|
||||
"series_id": "ECBDFR",
|
||||
"bank": "European Central Bank",
|
||||
"country": "EUR",
|
||||
"currency": "EUR",
|
||||
"label": "ECB Deposit Facility Rate",
|
||||
},
|
||||
"boe_bank_rate": {
|
||||
"series_id": "IUDSOIA",
|
||||
"bank": "Bank of England",
|
||||
"country": "GBR",
|
||||
"currency": "GBP",
|
||||
"label": "BoE Bank Rate (SONIA)",
|
||||
},
|
||||
}
|
||||
|
||||
_FRED_API_URL = "https://api.stlouisfed.org/fred/series/observations"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Curated fallback rates (updated periodically)
|
||||
# These are the most recent known policy rates for banks not on FRED.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_CURATED_RATES: list[dict] = [
|
||||
{"bank": "Bank of Japan", "country": "JPN", "currency": "JPY",
|
||||
"label": "BoJ Policy Rate", "rate": 0.50, "as_of": "2025-01-24",
|
||||
"notes": "Short-term policy rate target"},
|
||||
{"bank": "People's Bank of China", "country": "CHN", "currency": "CNY",
|
||||
"label": "PBoC LPR 1Y", "rate": 3.10, "as_of": "2025-01-20",
|
||||
"notes": "1-year Loan Prime Rate"},
|
||||
{"bank": "Reserve Bank of India", "country": "IND", "currency": "INR",
|
||||
"label": "RBI Repo Rate", "rate": 6.50, "as_of": "2025-02-07",
|
||||
"notes": "Policy repo rate"},
|
||||
{"bank": "Reserve Bank of Australia", "country": "AUS", "currency": "AUD",
|
||||
"label": "RBA Cash Rate", "rate": 4.35, "as_of": "2024-11-05",
|
||||
"notes": "Cash rate target"},
|
||||
{"bank": "Bank of Canada", "country": "CAN", "currency": "CAD",
|
||||
"label": "BoC Overnight Rate", "rate": 3.25, "as_of": "2024-12-11",
|
||||
"notes": "Target for the overnight rate"},
|
||||
{"bank": "Swiss National Bank", "country": "CHE", "currency": "CHF",
|
||||
"label": "SNB Policy Rate", "rate": 0.50, "as_of": "2024-12-12",
|
||||
"notes": "SNB policy rate"},
|
||||
{"bank": "Central Bank of Brazil", "country": "BRA", "currency": "BRL",
|
||||
"label": "BCB SELIC", "rate": 13.25, "as_of": "2025-01-29",
|
||||
"notes": "SELIC target rate"},
|
||||
{"bank": "Bank of Korea", "country": "KOR", "currency": "KRW",
|
||||
"label": "BoK Base Rate", "rate": 3.00, "as_of": "2025-01-16",
|
||||
"notes": "Base rate"},
|
||||
{"bank": "Central Bank of Turkey", "country": "TUR", "currency": "TRY",
|
||||
"label": "CBRT Policy Rate", "rate": 45.00, "as_of": "2025-01-23",
|
||||
"notes": "1-week repo rate"},
|
||||
{"bank": "South African Reserve Bank", "country": "ZAF", "currency": "ZAR",
|
||||
"label": "SARB Repo Rate", "rate": 7.75, "as_of": "2024-11-21",
|
||||
"notes": "Repurchase rate"},
|
||||
{"bank": "Banco de México", "country": "MEX", "currency": "MXN",
|
||||
"label": "Banxico Target Rate", "rate": 10.00, "as_of": "2025-02-06",
|
||||
"notes": "Overnight interbank rate target"},
|
||||
{"bank": "Bank Indonesia", "country": "IDN", "currency": "IDR",
|
||||
"label": "BI Rate", "rate": 5.75, "as_of": "2025-01-15",
|
||||
"notes": "BI-Rate"},
|
||||
]
|
||||
|
||||
|
||||
def _utc_now_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
async def _fetch_fred_rate(fetcher: Fetcher, series_id: str, meta: dict) -> dict | None:
|
||||
"""Fetch latest observation for a FRED series."""
|
||||
api_key = os.environ.get("FRED_API_KEY")
|
||||
if not api_key:
|
||||
return None
|
||||
|
||||
data = await fetcher.get_json(
|
||||
_FRED_API_URL,
|
||||
source="fred",
|
||||
cache_key=f"central_banks:fred:{series_id}",
|
||||
cache_ttl=3600,
|
||||
params={
|
||||
"series_id": series_id,
|
||||
"api_key": api_key,
|
||||
"file_type": "json",
|
||||
"sort_order": "desc",
|
||||
"limit": "1",
|
||||
},
|
||||
)
|
||||
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
obs = data["observations"][0]
|
||||
value = obs.get("value", ".")
|
||||
if value == ".":
|
||||
return None
|
||||
return {
|
||||
"bank": meta["bank"],
|
||||
"country": meta["country"],
|
||||
"currency": meta["currency"],
|
||||
"label": meta["label"],
|
||||
"rate": float(value),
|
||||
"as_of": obs.get("date"),
|
||||
"source": "fred",
|
||||
}
|
||||
except (KeyError, IndexError, TypeError, ValueError):
|
||||
logger.warning("FRED parse error for %s", series_id)
|
||||
return None
|
||||
|
||||
|
||||
async def fetch_central_bank_rates(fetcher: Fetcher) -> dict:
|
||||
"""Fetch policy rates for 15 major central banks.
|
||||
|
||||
Uses FRED API for Fed, ECB, and BoE when FRED_API_KEY is set.
|
||||
Falls back to curated static data for all other banks.
|
||||
|
||||
Returns::
|
||||
|
||||
{"rates": [{bank, country, currency, rate, as_of, source}],
|
||||
"count": int, "source": "multi", "timestamp": "<iso>"}
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
# Try FRED for the banks we have series for
|
||||
fred_tasks = []
|
||||
for key, meta in _FRED_RATE_SERIES.items():
|
||||
fred_tasks.append(_fetch_fred_rate(fetcher, meta["series_id"], meta))
|
||||
|
||||
fred_results = await asyncio.gather(*fred_tasks)
|
||||
|
||||
rates: list[dict] = []
|
||||
fred_banks: set[str] = set()
|
||||
|
||||
for result in fred_results:
|
||||
if result is not None:
|
||||
rates.append(result)
|
||||
fred_banks.add(result["bank"])
|
||||
|
||||
# Add curated rates (skip any that FRED already provided)
|
||||
for entry in _CURATED_RATES:
|
||||
if entry["bank"] not in fred_banks:
|
||||
rates.append({**entry, "source": "curated"})
|
||||
|
||||
# If FRED was unavailable, add curated versions of FRED banks too
|
||||
fred_curated_fallback = [
|
||||
{"bank": "Federal Reserve", "country": "USA", "currency": "USD",
|
||||
"label": "Federal Funds Rate", "rate": 4.50, "as_of": "2025-01-29",
|
||||
"notes": "Fed funds target rate (upper bound)"},
|
||||
{"bank": "European Central Bank", "country": "EUR", "currency": "EUR",
|
||||
"label": "ECB Deposit Facility Rate", "rate": 2.75, "as_of": "2025-01-30",
|
||||
"notes": "Deposit facility rate"},
|
||||
{"bank": "Bank of England", "country": "GBR", "currency": "GBP",
|
||||
"label": "BoE Bank Rate", "rate": 4.50, "as_of": "2025-02-06",
|
||||
"notes": "Bank rate"},
|
||||
]
|
||||
for entry in fred_curated_fallback:
|
||||
if entry["bank"] not in fred_banks:
|
||||
rates.append({**entry, "source": "curated"})
|
||||
|
||||
# Sort by rate descending (most hawkish first)
|
||||
rates.sort(key=lambda r: r.get("rate", 0), reverse=True)
|
||||
|
||||
return {
|
||||
"rates": rates,
|
||||
"count": len(rates),
|
||||
"fred_available": bool(os.environ.get("FRED_API_KEY")),
|
||||
"source": "multi",
|
||||
"timestamp": _utc_now_iso(),
|
||||
}
|
||||
@@ -22,6 +22,7 @@ from ..config.datacenters import AI_DATACENTERS, query_datacenters
|
||||
from ..config.spaceports import SPACEPORTS, query_spaceports
|
||||
from ..config.minerals import CRITICAL_MINERALS, query_minerals
|
||||
from ..config.exchanges import STOCK_EXCHANGES, query_exchanges
|
||||
from ..config.trade_routes import TRADE_ROUTES, CLOUD_REGIONS, FINANCIAL_CENTERS
|
||||
|
||||
|
||||
def _utc_now_iso() -> str:
|
||||
@@ -295,3 +296,102 @@ async def fetch_stock_exchanges(
|
||||
"source": "static-geospatial",
|
||||
"timestamp": _utc_now_iso(),
|
||||
}
|
||||
|
||||
|
||||
async def fetch_trade_routes(
|
||||
route_type: str | None = None,
|
||||
country: str | None = None,
|
||||
) -> dict:
|
||||
"""Query maritime trade routes and chokepoints.
|
||||
|
||||
Args:
|
||||
route_type: Filter by type (chokepoint, canal, route).
|
||||
country: Filter by ISO-3 country code in the countries list.
|
||||
"""
|
||||
routes = list(TRADE_ROUTES)
|
||||
if route_type:
|
||||
routes = [r for r in routes if r["type"] == route_type.lower()]
|
||||
if country:
|
||||
c = country.upper()
|
||||
routes = [r for r in routes if c in r.get("countries", [])]
|
||||
|
||||
total_oil = sum(r.get("oil_flow_mbd", 0) for r in routes)
|
||||
by_type: dict[str, int] = {}
|
||||
for r in routes:
|
||||
by_type[r["type"]] = by_type.get(r["type"], 0) + 1
|
||||
|
||||
return {
|
||||
"routes": routes,
|
||||
"count": len(routes),
|
||||
"total_in_database": len(TRADE_ROUTES),
|
||||
"total_oil_flow_mbd": round(total_oil, 1),
|
||||
"by_type": by_type,
|
||||
"filters": {"route_type": route_type, "country": country},
|
||||
"source": "static-geospatial",
|
||||
"timestamp": _utc_now_iso(),
|
||||
}
|
||||
|
||||
|
||||
async def fetch_cloud_regions(
|
||||
provider: str | None = None,
|
||||
country: str | None = None,
|
||||
) -> dict:
|
||||
"""Query major cloud provider regions (AWS, Azure, GCP).
|
||||
|
||||
Args:
|
||||
provider: Filter by provider (AWS, Azure, GCP).
|
||||
country: Filter by region name substring.
|
||||
"""
|
||||
regions = list(CLOUD_REGIONS)
|
||||
if provider:
|
||||
p = provider.upper()
|
||||
regions = [r for r in regions if r["provider"].upper() == p]
|
||||
if country:
|
||||
c = country.lower()
|
||||
regions = [r for r in regions if c in r["name"].lower()]
|
||||
|
||||
by_provider: dict[str, int] = {}
|
||||
for r in regions:
|
||||
by_provider[r["provider"]] = by_provider.get(r["provider"], 0) + 1
|
||||
|
||||
return {
|
||||
"regions": regions,
|
||||
"count": len(regions),
|
||||
"total_in_database": len(CLOUD_REGIONS),
|
||||
"by_provider": by_provider,
|
||||
"filters": {"provider": provider, "country": country},
|
||||
"source": "static-geospatial",
|
||||
"timestamp": _utc_now_iso(),
|
||||
}
|
||||
|
||||
|
||||
async def fetch_financial_centers(
|
||||
country: str | None = None,
|
||||
min_rank: int | None = None,
|
||||
) -> dict:
|
||||
"""Query global financial centers (GFCI top 20+).
|
||||
|
||||
Args:
|
||||
country: Filter by ISO-3 country code.
|
||||
min_rank: Only include centers ranked this or better (lower = better).
|
||||
"""
|
||||
centers = list(FINANCIAL_CENTERS)
|
||||
if country:
|
||||
c = country.upper()
|
||||
centers = [fc for fc in centers if fc["iso3"] == c]
|
||||
if min_rank is not None:
|
||||
centers = [fc for fc in centers if fc["gfci_rank"] <= min_rank]
|
||||
|
||||
by_country: dict[str, int] = {}
|
||||
for fc in centers:
|
||||
by_country[fc["iso3"]] = by_country.get(fc["iso3"], 0) + 1
|
||||
|
||||
return {
|
||||
"centers": centers,
|
||||
"count": len(centers),
|
||||
"total_in_database": len(FINANCIAL_CENTERS),
|
||||
"by_country": by_country,
|
||||
"filters": {"country": country, "min_rank": min_rank},
|
||||
"source": "static-geospatial",
|
||||
"timestamp": _utc_now_iso(),
|
||||
}
|
||||
|
||||
@@ -446,6 +446,72 @@ async def fetch_country_stocks(
|
||||
}
|
||||
|
||||
|
||||
async def fetch_btc_technicals(fetcher: Fetcher) -> dict:
|
||||
"""Compute Bitcoin technical indicators from CoinGecko historical data.
|
||||
|
||||
Calculates SMA-50, SMA-200, Mayer Multiple (price/SMA200), golden/death
|
||||
cross status, and distance from all-time high.
|
||||
|
||||
Returns::
|
||||
|
||||
{"price": float, "sma_50": float, "sma_200": float,
|
||||
"mayer_multiple": float, "cross_signal": str, ...}
|
||||
"""
|
||||
url = "https://api.coingecko.com/api/v3/coins/bitcoin/market_chart"
|
||||
data = await fetcher.get_json(
|
||||
url,
|
||||
source="coingecko",
|
||||
cache_key="markets:btc_technicals",
|
||||
cache_ttl=600,
|
||||
params={"vs_currency": "usd", "days": "200", "interval": "daily"},
|
||||
)
|
||||
|
||||
if data is None or "prices" not in data:
|
||||
return {"error": "Failed to fetch BTC historical data", "source": "coingecko", "timestamp": _utc_now_iso()}
|
||||
|
||||
prices = [p[1] for p in data["prices"]]
|
||||
if len(prices) < 50:
|
||||
return {"error": "Insufficient price history", "source": "coingecko", "timestamp": _utc_now_iso()}
|
||||
|
||||
current_price = prices[-1]
|
||||
|
||||
sma_50 = sum(prices[-50:]) / 50
|
||||
sma_200 = sum(prices[-200:]) / min(len(prices), 200) if len(prices) >= 50 else None
|
||||
|
||||
mayer_multiple = round(current_price / sma_200, 4) if sma_200 and sma_200 > 0 else None
|
||||
|
||||
# Golden cross: SMA50 > SMA200, Death cross: SMA50 < SMA200
|
||||
cross_signal = "neutral"
|
||||
if sma_200 is not None:
|
||||
if sma_50 > sma_200:
|
||||
cross_signal = "golden_cross"
|
||||
elif sma_50 < sma_200:
|
||||
cross_signal = "death_cross"
|
||||
|
||||
# Distance from ATH
|
||||
ath = max(prices)
|
||||
ath_distance_pct = round(((current_price - ath) / ath) * 100, 2) if ath > 0 else None
|
||||
|
||||
# 7d and 30d price change
|
||||
change_7d = round(((current_price - prices[-8]) / prices[-8]) * 100, 2) if len(prices) >= 8 else None
|
||||
change_30d = round(((current_price - prices[-31]) / prices[-31]) * 100, 2) if len(prices) >= 31 else None
|
||||
|
||||
return {
|
||||
"price": round(current_price, 2),
|
||||
"sma_50": round(sma_50, 2),
|
||||
"sma_200": round(sma_200, 2) if sma_200 else None,
|
||||
"mayer_multiple": mayer_multiple,
|
||||
"cross_signal": cross_signal,
|
||||
"ath_in_period": round(ath, 2),
|
||||
"ath_distance_pct": ath_distance_pct,
|
||||
"change_7d_pct": change_7d,
|
||||
"change_30d_pct": change_30d,
|
||||
"data_points": len(prices),
|
||||
"source": "coingecko",
|
||||
"timestamp": _utc_now_iso(),
|
||||
}
|
||||
|
||||
|
||||
async def fetch_macro_signals(fetcher: Fetcher) -> dict:
|
||||
"""Aggregate 7 macro signals into a single dashboard payload.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user